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/no-extra-parens rule #1158

Merged
merged 4 commits into from May 23, 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 @@ -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
}
}