Skip to content

Commit e23f506

Browse files
committed
New: Add vue/valid-v-bind-sync rule
1 parent 501a409 commit e23f506

File tree

7 files changed

+418
-0
lines changed

7 files changed

+418
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
237237
| :wrench: | [vue/no-spaces-around-equal-signs-in-attribute](./docs/rules/no-spaces-around-equal-signs-in-attribute.md) | disallow spaces around equal signs in attribute |
238238
| :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
239239
| :wrench: | [vue/singleline-html-element-content-newline](./docs/rules/singleline-html-element-content-newline.md) | require a line break before and after the contents of a singleline element |
240+
| | [vue/valid-v-bind-sync](./docs/rules/valid-v-bind-sync.md) | enforce valid `.sync` modifier on `v-bind` directives |
240241

241242
### Deprecated
242243

docs/rules/valid-v-bind-sync.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# enforce valid `.sync` modifier on `v-bind` directives (vue/valid-v-bind-sync)
2+
3+
This rule checks whether every `.sync` modifier on `v-bind` directives is valid.
4+
5+
## :book: Rule Details
6+
7+
This rule reports `.sync` modifier on `v-bind` directives in the following cases:
8+
9+
- The directive does not have the attribute value which is valid as LHS. E.g. `<MyComponent v-bind:aaa.sync="foo() + bar()" />`
10+
- The directive is on non Vue-components. E.g. `<input v-bind:aaa.sync="foo"></div>`
11+
- The directive's reference is iteration variables. E.g. `<div v-for="x in list"><MyComponent v-bind:aaa.sync="x" /></div>`
12+
13+
This rule does not check syntax errors in directives because it's checked by [no-parsing-error] rule.
14+
15+
```vue
16+
<MyComponent v-bind:aaa.sync="foo + bar" />
17+
<MyComponent :aaa.sync="foo + bar" />
18+
19+
<input v-bind:aaa.sync="foo">
20+
<input :aaa.sync="foo">
21+
22+
<div v-for="todo in todos">
23+
<MyComponent v-bind:aaa.sync="todo" />
24+
<MyComponent :aaa.sync="todo" />
25+
</div>
26+
```
27+
28+
:+1: Examples of **correct** code for this rule:
29+
30+
```vue
31+
<MyComponent v-bind:aaa.sync="foo"/>
32+
<MyComponent :aaa.sync="foo"/>
33+
34+
<div v-for="todo in todos">
35+
<MyComponent v-bind:aaa.sync="todo.name"/>
36+
<MyComponent :aaa.sync="todo.name"/>
37+
</div>
38+
```
39+
40+
## :wrench: Options
41+
42+
Nothing.
43+
44+
## :couple: Related rules
45+
46+
- [no-parsing-error]
47+
48+
[no-parsing-error]: no-parsing-error.md

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ module.exports = {
5555
'v-bind-style': require('./rules/v-bind-style'),
5656
'v-on-style': require('./rules/v-on-style'),
5757
'valid-template-root': require('./rules/valid-template-root'),
58+
'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
5859
'valid-v-bind': require('./rules/valid-v-bind'),
5960
'valid-v-cloak': require('./rules/valid-v-cloak'),
6061
'valid-v-else-if': require('./rules/valid-v-else-if'),

lib/rules/valid-v-bind-sync.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* @fileoverview enforce valid `.sync` modifier on `v-bind` directives
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
/**
18+
* Check whether the given node is valid or not.
19+
* @param {ASTNode} node The element node to check.
20+
* @returns {boolean} `true` if the node is valid.
21+
*/
22+
function isValidElement (node) {
23+
if (
24+
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
25+
utils.isHtmlWellKnownElementName(node.rawName) ||
26+
utils.isSvgWellKnownElementName(node.rawName)
27+
) {
28+
// non Vue-component
29+
return false
30+
}
31+
return true
32+
}
33+
34+
/**
35+
* Check whether the given node can be LHS.
36+
* @param {ASTNode} node The node to check.
37+
* @returns {boolean} `true` if the node can be LHS.
38+
*/
39+
function isLhs (node) {
40+
return node != null && (
41+
node.type === 'Identifier' ||
42+
node.type === 'MemberExpression'
43+
)
44+
}
45+
46+
/**
47+
* Get the variable by names.
48+
* @param {string} name The variable name to find.
49+
* @param {ASTNode} leafNode The node to look up.
50+
* @returns {Variable|null} The found variable or null.
51+
*/
52+
function getVariable (name, leafNode) {
53+
let node = leafNode
54+
55+
while (node != null) {
56+
const variables = node.variables
57+
const variable = variables && variables.find(v => v.id.name === name)
58+
59+
if (variable != null) {
60+
return variable
61+
}
62+
63+
node = node.parent
64+
}
65+
66+
return null
67+
}
68+
69+
// ------------------------------------------------------------------------------
70+
// Rule Definition
71+
// ------------------------------------------------------------------------------
72+
73+
module.exports = {
74+
meta: {
75+
docs: {
76+
description: 'enforce valid `.sync` modifier on `v-bind` directives',
77+
category: undefined,
78+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/valid-v-bind-sync.md'
79+
},
80+
fixable: null,
81+
schema: []
82+
},
83+
84+
create (context) {
85+
return utils.defineTemplateBodyVisitor(context, {
86+
"VAttribute[directive=true][key.name='bind']" (node) {
87+
if (node.key.modifiers.indexOf('sync') < 0) {
88+
return
89+
}
90+
const element = node.parent.parent
91+
const name = element.name
92+
93+
if (!isValidElement(element)) {
94+
context.report({
95+
node,
96+
loc: node.loc,
97+
message: "'.sync' modifiers aren't supported on <{{name}}> non Vue-components.",
98+
data: { name }
99+
})
100+
}
101+
102+
if (node.value) {
103+
if (!isLhs(node.value.expression)) {
104+
context.report({
105+
node,
106+
loc: node.loc,
107+
message: "'.sync' modifiers require the attribute value which is valid as LHS."
108+
})
109+
}
110+
111+
for (const reference of node.value.references) {
112+
const id = reference.id
113+
if (id.parent.type !== 'VExpressionContainer') {
114+
continue
115+
}
116+
117+
const variable = getVariable(id.name, element)
118+
if (variable != null) {
119+
context.report({
120+
node,
121+
loc: node.loc,
122+
message: "'.sync' modifiers cannot update the iteration variable '{{varName}}' itself.",
123+
data: { varName: id.name }
124+
})
125+
}
126+
}
127+
}
128+
}
129+
})
130+
}
131+
}

lib/utils/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
const HTML_ELEMENT_NAMES = new Set(require('./html-elements.json'))
1313
const VOID_ELEMENT_NAMES = new Set(require('./void-elements.json'))
14+
const SVG_ELEMENT_NAMES = new Set(require('./svg-elements.json'))
1415
const assert = require('assert')
1516
const vueEslintParser = require('vue-eslint-parser')
1617

@@ -283,6 +284,16 @@ module.exports = {
283284
return HTML_ELEMENT_NAMES.has(name)
284285
},
285286

287+
/**
288+
* Check whether the given name is an well-known SVG element or not.
289+
* @param {string} name The name to check.
290+
* @returns {boolean} `true` if the name is an well-known SVG element name.
291+
*/
292+
isSvgWellKnownElementName (name) {
293+
assert(typeof name === 'string')
294+
return SVG_ELEMENT_NAMES.has(name)
295+
},
296+
286297
/**
287298
* Check whether the given name is a void element name or not.
288299
* @param {string} name The name to check.

lib/utils/svg-elements.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["a","animate","animateMotion","animateTransform","audio","canvas","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","iframe","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tspan","unknown","use","video","view"]

0 commit comments

Comments
 (0)