Skip to content

Commit 2606a02

Browse files
authored
Add vue/no-extra-parens rule (#1158)
* Add `vue/no-extra-parens` rule * update * update
1 parent f537354 commit 2606a02

File tree

6 files changed

+420
-0
lines changed

6 files changed

+420
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ For example:
292292
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
293293
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
294294
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
295+
| [vue/no-extra-parens](./no-extra-parens.md) | disallow unnecessary parentheses | :wrench: |
295296
| [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | |
296297
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
297298
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |

Diff for: docs/rules/no-extra-parens.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-extra-parens
5+
description: disallow unnecessary parentheses
6+
---
7+
# vue/no-extra-parens
8+
> disallow unnecessary parentheses
9+
10+
- :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.
11+
12+
This rule is the same rule as core [no-extra-parens] rule but it applies to the expressions in `<template>`.
13+
14+
## :book: Rule Details
15+
16+
This rule restricts the use of parentheses to only where they are necessary.
17+
This rule extends the core [no-extra-parens] rule and applies it to the `<template>`. This rule also checks some Vue.js syntax.
18+
19+
<eslint-code-block fix :rules="{'vue/no-extra-parens': ['error']}">
20+
21+
```vue
22+
<template>
23+
<!-- ✓ GOOD -->
24+
<div :class="foo + bar" />
25+
{{ foo + bar }}
26+
{{ foo + bar | filter }}
27+
<!-- ✗ BAD -->
28+
<div :class="(foo + bar)" />
29+
{{ (foo + bar) }}
30+
{{ (foo + bar) | filter }}
31+
</template>
32+
```
33+
34+
</eslint-code-block>
35+
36+
## :books: Further reading
37+
38+
- [no-extra-parens]
39+
40+
[no-extra-parens]: https://eslint.org/docs/rules/no-extra-parens
41+
42+
## :mag: Implementation
43+
44+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-extra-parens.js)
45+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-extra-parens.js)

Diff for: lib/configs/no-layout-rules.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module.exports = {
2727
'vue/max-len': 'off',
2828
'vue/multiline-html-element-content-newline': 'off',
2929
'vue/mustache-interpolation-spacing': 'off',
30+
'vue/no-extra-parens': 'off',
3031
'vue/no-multi-spaces': 'off',
3132
'vue/no-spaces-around-equal-signs-in-attribute': 'off',
3233
'vue/object-curly-spacing': 'off',

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ module.exports = {
6464
'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
6565
'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
6666
'no-empty-pattern': require('./rules/no-empty-pattern'),
67+
'no-extra-parens': require('./rules/no-extra-parens'),
6768
'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
6869
'no-lifecycle-after-await': require('./rules/no-lifecycle-after-await'),
6970
'no-multi-spaces': require('./rules/no-multi-spaces'),

Diff for: lib/rules/no-extra-parens.js

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* @author Yosuke Ota
3+
*/
4+
'use strict'
5+
6+
const { isParenthesized } = require('eslint-utils')
7+
const { wrapCoreRule } = require('../utils')
8+
9+
// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
10+
module.exports = wrapCoreRule(
11+
require('eslint/lib/rules/no-extra-parens'),
12+
{
13+
skipDynamicArguments: true,
14+
create: createForVueSyntax
15+
}
16+
)
17+
18+
/**
19+
* @typedef {import('vue-eslint-parser').AST.Token} Token
20+
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
21+
* @typedef {import('vue-eslint-parser').AST.VExpressionContainer} VExpressionContainer
22+
* @typedef {import('vue-eslint-parser').AST.VFilterSequenceExpression} VFilterSequenceExpression
23+
*/
24+
25+
/**
26+
* Check whether the given token is a left parenthesis.
27+
* @param {Token} token The token to check.
28+
* @returns {boolean} `true` if the token is a left parenthesis.
29+
*/
30+
function isLeftParen (token) {
31+
return token.type === 'Punctuator' && token.value === '('
32+
}
33+
34+
/**
35+
* Check whether the given token is a right parenthesis.
36+
* @param {Token} token The token to check.
37+
* @returns {boolean} `true` if the token is a right parenthesis.
38+
*/
39+
function isRightParen (token) {
40+
return token.type === 'Punctuator' && token.value === ')'
41+
}
42+
43+
/**
44+
* Check whether the given token is a left brace.
45+
* @param {Token} token The token to check.
46+
* @returns {boolean} `true` if the token is a left brace.
47+
*/
48+
function isLeftBrace (token) {
49+
return token.type === 'Punctuator' && token.value === '{'
50+
}
51+
52+
/**
53+
* Check whether the given token is a right brace.
54+
* @param {Token} token The token to check.
55+
* @returns {boolean} `true` if the token is a right brace.
56+
*/
57+
function isRightBrace (token) {
58+
return token.type === 'Punctuator' && token.value === '}'
59+
}
60+
61+
/**
62+
* Check whether the given token is a left bracket.
63+
* @param {Token} token The token to check.
64+
* @returns {boolean} `true` if the token is a left bracket.
65+
*/
66+
function isLeftBracket (token) {
67+
return token.type === 'Punctuator' && token.value === '['
68+
}
69+
70+
/**
71+
* Check whether the given token is a right bracket.
72+
* @param {Token} token The token to check.
73+
* @returns {boolean} `true` if the token is a right bracket.
74+
*/
75+
function isRightBracket (token) {
76+
return token.type === 'Punctuator' && token.value === ']'
77+
}
78+
79+
/**
80+
* Determines if a given expression node is an IIFE
81+
* @param {ASTNode} node The node to check
82+
* @returns {boolean} `true` if the given node is an IIFE
83+
*/
84+
function isIIFE (node) {
85+
return node.type === 'CallExpression' && node.callee.type === 'FunctionExpression'
86+
}
87+
88+
function createForVueSyntax (context) {
89+
const tokenStore = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
90+
91+
/**
92+
* Checks if the given node turns into a filter when unwraped.
93+
* @param {Expression} node node to evaluate
94+
* @returns {boolean} `true` if the given node turns into a filter when unwraped.
95+
*/
96+
function isUnwrapChangeToFilter (expression) {
97+
let parenStack = null
98+
for (const token of tokenStore.getTokens(expression)) {
99+
if (!parenStack) {
100+
if (token.value === '|') {
101+
return true
102+
}
103+
} else {
104+
if (parenStack.isUpToken(token)) {
105+
parenStack = parenStack.upper
106+
continue
107+
}
108+
}
109+
if (isLeftParen(token)) {
110+
parenStack = { isUpToken: isRightParen, upper: parenStack }
111+
} else if (isLeftBracket(token)) {
112+
parenStack = { isUpToken: isRightBracket, upper: parenStack }
113+
} else if (isLeftBrace(token)) {
114+
parenStack = { isUpToken: isRightBrace, upper: parenStack }
115+
}
116+
}
117+
return false
118+
}
119+
/**
120+
* @param {VExpressionContainer} node
121+
*/
122+
function verify (node) {
123+
let expression = node.expression
124+
if (!expression) {
125+
return
126+
}
127+
128+
if (expression.type === 'VFilterSequenceExpression') {
129+
expression = expression.expression
130+
}
131+
132+
if (!isParenthesized(expression, tokenStore)) {
133+
return
134+
}
135+
136+
if (!isParenthesized(2, expression, tokenStore)) {
137+
if (isIIFE(expression) && !isParenthesized(expression.callee, tokenStore)) {
138+
return
139+
}
140+
if (isUnwrapChangeToFilter(expression)) {
141+
return
142+
}
143+
}
144+
report(expression)
145+
}
146+
147+
/**
148+
* Report the node
149+
* @param {Expression} node node to evaluate
150+
* @returns {void}
151+
* @private
152+
*/
153+
function report (node) {
154+
const sourceCode = context.getSourceCode()
155+
const leftParenToken = tokenStore.getTokenBefore(node)
156+
const rightParenToken = tokenStore.getTokenAfter(node)
157+
158+
context.report({
159+
node,
160+
loc: leftParenToken.loc,
161+
messageId: 'unexpected',
162+
fix (fixer) {
163+
const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0])
164+
165+
return fixer.replaceTextRange([
166+
leftParenToken.range[0],
167+
rightParenToken.range[1]
168+
], parenthesizedSource)
169+
}
170+
})
171+
}
172+
173+
return {
174+
"VAttribute[directive=true][key.name.name='bind'] > VExpressionContainer": verify,
175+
'VElement > VExpressionContainer': verify
176+
}
177+
}

0 commit comments

Comments
 (0)