Skip to content

Commit 543361b

Browse files
authored
Fixed false positives for v-bind="object" syntax in vue/attributes-order rule (#1391)
1 parent caab1c4 commit 543361b

File tree

3 files changed

+440
-56
lines changed

3 files changed

+440
-56
lines changed

docs/rules/attributes-order.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,33 @@ This rule aims to enforce ordering of component attributes. The default order is
9191

9292
</eslint-code-block>
9393

94+
Note that `v-bind="object"` syntax is considered to be the same as the next or previous attribute categories.
95+
96+
<eslint-code-block fix :rules="{'vue/attributes-order': ['error']}">
97+
98+
```vue
99+
<template>
100+
<!-- ✓ GOOD (`v-bind="object"` is considered GLOBAL category) -->
101+
<MyComponent
102+
v-bind="object"
103+
id="x"
104+
v-model="x"
105+
v-bind:foo="x">
106+
</MyComponent>
107+
108+
<!-- ✗ BAD (`v-bind="object"` is considered UNIQUE category) -->
109+
<MyComponent
110+
key="x"
111+
v-model="x"
112+
v-bind="object">
113+
</MyComponent>
114+
</template>
115+
```
116+
117+
</eslint-code-block>
118+
94119
## :wrench: Options
120+
95121
```json
96122
{
97123
"vue/attributes-order": ["error", {
@@ -113,7 +139,7 @@ This rule aims to enforce ordering of component attributes. The default order is
113139
}
114140
```
115141

116-
### `"alphabetical": true`
142+
### `"alphabetical": true`
117143

118144
<eslint-code-block fix :rules="{'vue/attributes-order': ['error', {alphabetical: true}]}">
119145

lib/rules/attributes-order.js

+143-55
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ const utils = require('../utils')
88
// ------------------------------------------------------------------------------
99
// Rule Definition
1010
// ------------------------------------------------------------------------------
11+
12+
/**
13+
* @typedef { VDirective & { key: VDirectiveKey & { name: VIdentifier & { name: 'bind' } } } } VBindDirective
14+
*/
15+
1116
const ATTRS = {
1217
DEFINITION: 'DEFINITION',
1318
LIST_RENDERING: 'LIST_RENDERING',
@@ -22,13 +27,47 @@ const ATTRS = {
2227
CONTENT: 'CONTENT'
2328
}
2429

30+
/**
31+
* Check whether the given attribute is `v-bind` directive.
32+
* @param {VAttribute | VDirective | undefined | null} node
33+
* @returns { node is VBindDirective }
34+
*/
35+
function isVBind(node) {
36+
return Boolean(node && node.directive && node.key.name.name === 'bind')
37+
}
38+
/**
39+
* Check whether the given attribute is plain attribute.
40+
* @param {VAttribute | VDirective | undefined | null} node
41+
* @returns { node is VAttribute }
42+
*/
43+
function isVAttribute(node) {
44+
return Boolean(node && !node.directive)
45+
}
46+
/**
47+
* Check whether the given attribute is plain attribute or `v-bind` directive.
48+
* @param {VAttribute | VDirective | undefined | null} node
49+
* @returns { node is VAttribute }
50+
*/
51+
function isVAttributeOrVBind(node) {
52+
return isVAttribute(node) || isVBind(node)
53+
}
54+
55+
/**
56+
* Check whether the given attribute is `v-bind="..."` directive.
57+
* @param {VAttribute | VDirective | undefined | null} node
58+
* @returns { node is VBindDirective }
59+
*/
60+
function isVBindObject(node) {
61+
return isVBind(node) && node.key.argument == null
62+
}
63+
2564
/**
2665
* @param {VAttribute | VDirective} attribute
2766
* @param {SourceCode} sourceCode
2867
*/
2968
function getAttributeName(attribute, sourceCode) {
3069
if (attribute.directive) {
31-
if (attribute.key.name.name === 'bind') {
70+
if (isVBind(attribute)) {
3271
return attribute.key.argument
3372
? sourceCode.getText(attribute.key.argument)
3473
: ''
@@ -62,7 +101,7 @@ function getDirectiveKeyName(directiveKey, sourceCode) {
62101
function getAttributeType(attribute, sourceCode) {
63102
let propName
64103
if (attribute.directive) {
65-
if (attribute.key.name.name !== 'bind') {
104+
if (!isVBind(attribute)) {
66105
const name = attribute.key.name.name
67106
if (name === 'for') {
68107
return ATTRS.LIST_RENDERING
@@ -130,24 +169,14 @@ function getPosition(attribute, attributePosition, sourceCode) {
130169
* @param {SourceCode} sourceCode
131170
*/
132171
function isAlphabetical(prevNode, currNode, sourceCode) {
133-
const isSameType =
134-
getAttributeType(prevNode, sourceCode) ===
135-
getAttributeType(currNode, sourceCode)
136-
if (isSameType) {
137-
const prevName = getAttributeName(prevNode, sourceCode)
138-
const currName = getAttributeName(currNode, sourceCode)
139-
if (prevName === currName) {
140-
const prevIsBind = Boolean(
141-
prevNode.directive && prevNode.key.name.name === 'bind'
142-
)
143-
const currIsBind = Boolean(
144-
currNode.directive && currNode.key.name.name === 'bind'
145-
)
146-
return prevIsBind <= currIsBind
147-
}
148-
return prevName < currName
172+
const prevName = getAttributeName(prevNode, sourceCode)
173+
const currName = getAttributeName(currNode, sourceCode)
174+
if (prevName === currName) {
175+
const prevIsBind = isVBind(prevNode)
176+
const currIsBind = isVBind(currNode)
177+
return prevIsBind <= currIsBind
149178
}
150-
return true
179+
return prevName < currName
151180
}
152181

153182
/**
@@ -186,16 +215,6 @@ function create(context) {
186215
} else attributePosition[item] = i
187216
})
188217

189-
/**
190-
* @typedef {object} State
191-
* @property {number} currentPosition
192-
* @property {VAttribute | VDirective} previousNode
193-
*/
194-
/**
195-
* @type {State | null}
196-
*/
197-
let state
198-
199218
/**
200219
* @param {VAttribute | VDirective} node
201220
* @param {VAttribute | VDirective} previousNode
@@ -213,43 +232,112 @@ function create(context) {
213232

214233
fix(fixer) {
215234
const attributes = node.parent.attributes
216-
const shiftAttrs = attributes.slice(
235+
236+
/** @type { (node: VAttribute | VDirective | undefined) => boolean } */
237+
let isMoveUp
238+
239+
if (isVBindObject(node)) {
240+
// prev, v-bind:foo, v-bind -> v-bind:foo, v-bind, prev
241+
isMoveUp = isVAttributeOrVBind
242+
} else if (isVAttributeOrVBind(node)) {
243+
// prev, v-bind, v-bind:foo -> v-bind, v-bind:foo, prev
244+
isMoveUp = isVBindObject
245+
} else {
246+
isMoveUp = () => false
247+
}
248+
249+
const previousNodes = attributes.slice(
217250
attributes.indexOf(previousNode),
218-
attributes.indexOf(node) + 1
251+
attributes.indexOf(node)
219252
)
253+
const moveUpNodes = [node]
254+
const moveDownNodes = []
255+
let index = 0
256+
while (previousNodes[index]) {
257+
const node = previousNodes[index++]
258+
if (isMoveUp(node)) {
259+
moveUpNodes.unshift(node)
260+
} else {
261+
moveDownNodes.push(node)
262+
}
263+
}
264+
const moveNodes = [...moveUpNodes, ...moveDownNodes]
220265

221-
return shiftAttrs.map((attr, i) => {
222-
const text =
223-
attr === previousNode
224-
? sourceCode.getText(node)
225-
: sourceCode.getText(shiftAttrs[i - 1])
226-
return fixer.replaceText(attr, text)
266+
return moveNodes.map((moveNode, index) => {
267+
const text = sourceCode.getText(moveNode)
268+
return fixer.replaceText(previousNodes[index] || node, text)
227269
})
228270
}
229271
})
230272
}
231273

232274
return utils.defineTemplateBodyVisitor(context, {
233-
VStartTag() {
234-
state = null
235-
},
236-
VAttribute(node) {
237-
let inAlphaOrder = true
238-
if (state && alphabetical) {
239-
inAlphaOrder = isAlphabetical(state.previousNode, node, sourceCode)
275+
VStartTag(node) {
276+
const attributes = node.attributes.filter((node, index, attributes) => {
277+
if (
278+
isVBindObject(node) &&
279+
(isVAttributeOrVBind(attributes[index - 1]) ||
280+
isVAttributeOrVBind(attributes[index + 1]))
281+
) {
282+
// In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax
283+
// as they behave differently if you change the order.
284+
return false
285+
}
286+
return true
287+
})
288+
if (attributes.length <= 1) {
289+
return
240290
}
241-
if (
242-
!state ||
243-
(state.currentPosition <=
244-
getPosition(node, attributePosition, sourceCode) &&
245-
inAlphaOrder)
246-
) {
247-
state = {
248-
currentPosition: getPosition(node, attributePosition, sourceCode),
249-
previousNode: node
291+
292+
let previousNode = attributes[0]
293+
let previousPosition = getPositionFromAttrIndex(0)
294+
for (let index = 1; index < attributes.length; index++) {
295+
const node = attributes[index]
296+
const position = getPositionFromAttrIndex(index)
297+
298+
let valid = previousPosition <= position
299+
if (valid && alphabetical && previousPosition === position) {
300+
valid = isAlphabetical(previousNode, node, sourceCode)
250301
}
251-
} else {
252-
reportIssue(node, state.previousNode)
302+
if (valid) {
303+
previousNode = node
304+
previousPosition = position
305+
} else {
306+
reportIssue(node, previousNode)
307+
}
308+
}
309+
310+
/**
311+
* @param {number} index
312+
* @returns {number}
313+
*/
314+
function getPositionFromAttrIndex(index) {
315+
const node = attributes[index]
316+
if (isVBindObject(node)) {
317+
// node is `v-bind ="object"` syntax
318+
319+
// In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`,
320+
// the behavior will be different, so adjust so that there is no change in behavior.
321+
322+
const len = attributes.length
323+
for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
324+
const next = attributes[nextIndex]
325+
326+
if (isVAttributeOrVBind(next) && !isVBindObject(next)) {
327+
// It is considered to be in the same order as the next bind prop node.
328+
return getPositionFromAttrIndex(nextIndex)
329+
}
330+
}
331+
for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) {
332+
const prev = attributes[prevIndex]
333+
334+
if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) {
335+
// It is considered to be in the same order as the prev bind prop node.
336+
return getPositionFromAttrIndex(prevIndex)
337+
}
338+
}
339+
}
340+
return getPosition(node, attributePosition, sourceCode)
253341
}
254342
}
255343
})

0 commit comments

Comments
 (0)