Skip to content

Commit

Permalink
Add vue/no-extra-parens rule (#1158)
Browse files Browse the repository at this point in the history
* Add `vue/no-extra-parens` rule

* update

* update
  • Loading branch information
ota-meshi committed May 23, 2020
1 parent f537354 commit 2606a02
Show file tree
Hide file tree
Showing 6 changed files with 420 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -292,6 +292,7 @@ For example:
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
| [vue/no-extra-parens](./no-extra-parens.md) | disallow unnecessary parentheses | :wrench: |
| [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | |
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
Expand Down
45 changes: 45 additions & 0 deletions docs/rules/no-extra-parens.md
@@ -0,0 +1,45 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-extra-parens
description: disallow unnecessary parentheses
---
# vue/no-extra-parens
> disallow unnecessary parentheses
- :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.

This rule is the same rule as core [no-extra-parens] rule but it applies to the expressions in `<template>`.

## :book: Rule Details

This rule restricts the use of parentheses to only where they are necessary.
This rule extends the core [no-extra-parens] rule and applies it to the `<template>`. This rule also checks some Vue.js syntax.

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

```vue
<template>
<!-- ✓ GOOD -->
<div :class="foo + bar" />
{{ foo + bar }}
{{ foo + bar | filter }}
<!-- ✗ BAD -->
<div :class="(foo + bar)" />
{{ (foo + bar) }}
{{ (foo + bar) | filter }}
</template>
```

</eslint-code-block>

## :books: Further reading

- [no-extra-parens]

[no-extra-parens]: https://eslint.org/docs/rules/no-extra-parens

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-extra-parens.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-extra-parens.js)
1 change: 1 addition & 0 deletions lib/configs/no-layout-rules.js
Expand Up @@ -27,6 +27,7 @@ module.exports = {
'vue/max-len': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/mustache-interpolation-spacing': 'off',
'vue/no-extra-parens': 'off',
'vue/no-multi-spaces': 'off',
'vue/no-spaces-around-equal-signs-in-attribute': 'off',
'vue/object-curly-spacing': 'off',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -64,6 +64,7 @@ module.exports = {
'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
'no-empty-pattern': require('./rules/no-empty-pattern'),
'no-extra-parens': require('./rules/no-extra-parens'),
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
'no-multi-spaces': require('./rules/no-multi-spaces'),
Expand Down
177 changes: 177 additions & 0 deletions lib/rules/no-extra-parens.js
@@ -0,0 +1,177 @@
/**
* @author Yosuke Ota
*/
'use strict'

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

// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
module.exports = wrapCoreRule(
require('eslint/lib/rules/no-extra-parens'),
{
skipDynamicArguments: true,
create: createForVueSyntax
}
)

/**
* @typedef {import('vue-eslint-parser').AST.Token} Token
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
* @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer
* @typedef {import('vue-eslint-parser').AST.VFilterSequenceExpression} VFilterSequenceExpression
*/

/**
* Check whether the given token is a left parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left parenthesis.
*/
function isLeftParen (token) {
return token.type === 'Punctuator' && token.value === '('
}

/**
* Check whether the given token is a right parenthesis.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right parenthesis.
*/
function isRightParen (token) {
return token.type === 'Punctuator' && token.value === ')'
}

/**
* Check whether the given token is a left brace.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left brace.
*/
function isLeftBrace (token) {
return token.type === 'Punctuator' && token.value === '{'
}

/**
* Check whether the given token is a right brace.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right brace.
*/
function isRightBrace (token) {
return token.type === 'Punctuator' && token.value === '}'
}

/**
* Check whether the given token is a left bracket.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a left bracket.
*/
function isLeftBracket (token) {
return token.type === 'Punctuator' && token.value === '['
}

/**
* Check whether the given token is a right bracket.
* @param {Token} token The token to check.
* @returns {boolean} `true` if the token is a right bracket.
*/
function isRightBracket (token) {
return token.type === 'Punctuator' && token.value === ']'
}

/**
* Determines if a given expression node is an IIFE
* @param {ASTNode} node The node to check
* @returns {boolean} `true` if the given node is an IIFE
*/
function isIIFE (node) {
return node.type === 'CallExpression' && node.callee.type === 'FunctionExpression'
}

function createForVueSyntax (context) {
const tokenStore = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()

/**
* Checks if the given node turns into a filter when unwraped.
* @param {Expression} node node to evaluate
* @returns {boolean} `true` if the given node turns into a filter when unwraped.
*/
function isUnwrapChangeToFilter (expression) {
let parenStack = null
for (const token of tokenStore.getTokens(expression)) {
if (!parenStack) {
if (token.value === '|') {
return true
}
} else {
if (parenStack.isUpToken(token)) {
parenStack = parenStack.upper
continue
}
}
if (isLeftParen(token)) {
parenStack = { isUpToken: isRightParen, upper: parenStack }
} else if (isLeftBracket(token)) {
parenStack = { isUpToken: isRightBracket, upper: parenStack }
} else if (isLeftBrace(token)) {
parenStack = { isUpToken: isRightBrace, upper: parenStack }
}
}
return false
}
/**
* @param {VExpressionContainer} node
*/
function verify (node) {
let expression = node.expression
if (!expression) {
return
}

if (expression.type === 'VFilterSequenceExpression') {
expression = expression.expression
}

if (!isParenthesized(expression, tokenStore)) {
return
}

if (!isParenthesized(2, expression, tokenStore)) {
if (isIIFE(expression) && !isParenthesized(expression.callee, tokenStore)) {
return
}
if (isUnwrapChangeToFilter(expression)) {
return
}
}
report(expression)
}

/**
* Report the node
* @param {Expression} node node to evaluate
* @returns {void}
* @private
*/
function report (node) {
const sourceCode = context.getSourceCode()
const leftParenToken = tokenStore.getTokenBefore(node)
const rightParenToken = tokenStore.getTokenAfter(node)

context.report({
node,
loc: leftParenToken.loc,
messageId: 'unexpected',
fix (fixer) {
const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0])

return fixer.replaceTextRange([
leftParenToken.range[0],
rightParenToken.range[1]
], parenthesizedSource)
}
})
}

return {
"VAttribute[directive=true][key.name.name='bind'] > VExpressionContainer": verify,
'VElement > VExpressionContainer': verify
}
}

0 comments on commit 2606a02

Please sign in to comment.