Skip to content

Commit 7965d12

Browse files
ota-meshiCyberAP
andauthored
Add "SLOT" option to vue/attributes-order rule to specify v-slot order. (#1429)
* feat(attribute-order): add slot attribute * update * revert option schema Co-authored-by: Stas Lashmanov <[email protected]>
1 parent cc9c140 commit 7965d12

File tree

3 files changed

+201
-57
lines changed

3 files changed

+201
-57
lines changed

docs/rules/attributes-order.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ This rule aims to enforce ordering of component attributes. The default order is
2727
- `GLOBAL`
2828
e.g. 'id'
2929
- `UNIQUE`
30-
e.g. 'ref', 'key', 'v-slot', 'slot'
30+
e.g. 'ref', 'key'
31+
- `SLOT`
32+
e.g. 'v-slot', 'slot'.
3133
- `TWO_WAY_BINDING`
3234
e.g. 'v-model'
3335
- `OTHER_DIRECTIVES`
@@ -127,7 +129,7 @@ Note that `v-bind="object"` syntax is considered to be the same as the next or p
127129
"CONDITIONALS",
128130
"RENDER_MODIFIERS",
129131
"GLOBAL",
130-
"UNIQUE",
132+
["UNIQUE", "SLOT"],
131133
"TWO_WAY_BINDING",
132134
"OTHER_DIRECTIVES",
133135
"OTHER_ATTR",

lib/rules/attributes-order.js

+76-55
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const ATTRS = {
2020
RENDER_MODIFIERS: 'RENDER_MODIFIERS',
2121
GLOBAL: 'GLOBAL',
2222
UNIQUE: 'UNIQUE',
23+
SLOT: 'SLOT',
2324
TWO_WAY_BINDING: 'TWO_WAY_BINDING',
2425
OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
2526
OTHER_ATTR: 'OTHER_ATTR',
@@ -121,7 +122,7 @@ function getAttributeType(attribute) {
121122
} else if (name === 'html' || name === 'text') {
122123
return ATTRS.CONTENT
123124
} else if (name === 'slot') {
124-
return ATTRS.UNIQUE
125+
return ATTRS.SLOT
125126
} else if (name === 'is') {
126127
return ATTRS.DEFINITION
127128
} else {
@@ -139,13 +140,10 @@ function getAttributeType(attribute) {
139140
return ATTRS.DEFINITION
140141
} else if (propName === 'id') {
141142
return ATTRS.GLOBAL
142-
} else if (
143-
propName === 'ref' ||
144-
propName === 'key' ||
145-
propName === 'slot' ||
146-
propName === 'slot-scope'
147-
) {
143+
} else if (propName === 'ref' || propName === 'key') {
148144
return ATTRS.UNIQUE
145+
} else if (propName === 'slot' || propName === 'slot-scope') {
146+
return ATTRS.SLOT
149147
} else {
150148
return ATTRS.OTHER_ATTR
151149
}
@@ -154,12 +152,13 @@ function getAttributeType(attribute) {
154152
/**
155153
* @param {VAttribute | VDirective} attribute
156154
* @param { { [key: string]: number } } attributePosition
155+
* @returns {number | null} If the value is null, the order is omitted. Do not force the order.
157156
*/
158157
function getPosition(attribute, attributePosition) {
159158
const attributeType = getAttributeType(attribute)
160159
return attributePosition[attributeType] != null
161160
? attributePosition[attributeType]
162-
: -1
161+
: null
163162
}
164163

165164
/**
@@ -190,7 +189,7 @@ function create(context) {
190189
ATTRS.CONDITIONALS,
191190
ATTRS.RENDER_MODIFIERS,
192191
ATTRS.GLOBAL,
193-
ATTRS.UNIQUE,
192+
[ATTRS.UNIQUE, ATTRS.SLOT],
194193
ATTRS.TWO_WAY_BINDING,
195194
ATTRS.OTHER_DIRECTIVES,
196195
ATTRS.OTHER_ATTR,
@@ -267,74 +266,96 @@ function create(context) {
267266

268267
return utils.defineTemplateBodyVisitor(context, {
269268
VStartTag(node) {
270-
const attributes = node.attributes.filter((node, index, attributes) => {
271-
if (
272-
isVBindObject(node) &&
273-
(isVAttributeOrVBind(attributes[index - 1]) ||
274-
isVAttributeOrVBind(attributes[index + 1]))
275-
) {
276-
// In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax
277-
// as they behave differently if you change the order.
278-
return false
279-
}
280-
return true
281-
})
282-
if (attributes.length <= 1) {
269+
const attributeAndPositions = getAttributeAndPositionList(node)
270+
if (attributeAndPositions.length <= 1) {
283271
return
284272
}
285273

286-
let previousNode = attributes[0]
287-
let previousPosition = getPositionFromAttrIndex(0)
288-
for (let index = 1; index < attributes.length; index++) {
289-
const node = attributes[index]
290-
const position = getPositionFromAttrIndex(index)
274+
let {
275+
attr: previousNode,
276+
position: previousPosition
277+
} = attributeAndPositions[0]
278+
for (let index = 1; index < attributeAndPositions.length; index++) {
279+
const { attr, position } = attributeAndPositions[index]
291280

292281
let valid = previousPosition <= position
293282
if (valid && alphabetical && previousPosition === position) {
294-
valid = isAlphabetical(previousNode, node, sourceCode)
283+
valid = isAlphabetical(previousNode, attr, sourceCode)
295284
}
296285
if (valid) {
297-
previousNode = node
286+
previousNode = attr
298287
previousPosition = position
299288
} else {
300-
reportIssue(node, previousNode)
289+
reportIssue(attr, previousNode)
301290
}
302291
}
292+
}
293+
})
303294

304-
/**
305-
* @param {number} index
306-
* @returns {number}
307-
*/
308-
function getPositionFromAttrIndex(index) {
309-
const node = attributes[index]
310-
if (isVBindObject(node)) {
311-
// node is `v-bind ="object"` syntax
295+
/**
296+
* @param {VStartTag} node
297+
* @returns { { attr: ( VAttribute | VDirective ), position: number }[] }
298+
*/
299+
function getAttributeAndPositionList(node) {
300+
const attributes = node.attributes.filter((node, index, attributes) => {
301+
if (
302+
isVBindObject(node) &&
303+
(isVAttributeOrVBind(attributes[index - 1]) ||
304+
isVAttributeOrVBind(attributes[index + 1]))
305+
) {
306+
// In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax
307+
// as they behave differently if you change the order.
308+
return false
309+
}
310+
return true
311+
})
312312

313-
// In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`,
314-
// the behavior will be different, so adjust so that there is no change in behavior.
313+
const results = []
314+
for (let index = 0; index < attributes.length; index++) {
315+
const attr = attributes[index]
316+
const position = getPositionFromAttrIndex(index)
317+
if (position == null) {
318+
// The omitted order is skipped.
319+
continue
320+
}
321+
results.push({ attr, position })
322+
}
323+
324+
return results
325+
326+
/**
327+
* @param {number} index
328+
* @returns {number | null}
329+
*/
330+
function getPositionFromAttrIndex(index) {
331+
const node = attributes[index]
332+
if (isVBindObject(node)) {
333+
// node is `v-bind ="object"` syntax
315334

316-
const len = attributes.length
317-
for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
318-
const next = attributes[nextIndex]
335+
// In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`,
336+
// the behavior will be different, so adjust so that there is no change in behavior.
319337

320-
if (isVAttributeOrVBind(next) && !isVBindObject(next)) {
321-
// It is considered to be in the same order as the next bind prop node.
322-
return getPositionFromAttrIndex(nextIndex)
323-
}
338+
const len = attributes.length
339+
for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
340+
const next = attributes[nextIndex]
341+
342+
if (isVAttributeOrVBind(next) && !isVBindObject(next)) {
343+
// It is considered to be in the same order as the next bind prop node.
344+
return getPositionFromAttrIndex(nextIndex)
324345
}
325-
for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) {
326-
const prev = attributes[prevIndex]
346+
}
347+
for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) {
348+
const prev = attributes[prevIndex]
327349

328-
if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) {
329-
// It is considered to be in the same order as the prev bind prop node.
330-
return getPositionFromAttrIndex(prevIndex)
331-
}
350+
if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) {
351+
// It is considered to be in the same order as the prev bind prop node.
352+
return getPositionFromAttrIndex(prevIndex)
332353
}
333354
}
334-
return getPosition(node, attributePosition)
335355
}
356+
return getPosition(node, attributePosition)
336357
}
337-
})
358+
}
338359
}
339360

340361
module.exports = {

tests/lib/rules/attributes-order.js

+121
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,20 @@ tester.run('attributes-order', rule, {
421421
</div>
422422
</template>`,
423423
options: [{ alphabetical: true }]
424+
},
425+
426+
// omit order
427+
{
428+
filename: 'test.vue',
429+
code: `
430+
<template>
431+
<div
432+
v-for="a in items"
433+
v-if="a"
434+
attr="a">
435+
</div>
436+
</template>`,
437+
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }]
424438
}
425439
],
426440

@@ -1213,6 +1227,113 @@ tester.run('attributes-order', rule, {
12131227
'Attribute "v-bind" should go before "v-on:click".',
12141228
'Attribute "v-if" should go before "v-on:click".'
12151229
]
1230+
},
1231+
1232+
// omit order
1233+
{
1234+
filename: 'test.vue',
1235+
code: `
1236+
<template>
1237+
<div
1238+
v-if="a"
1239+
attr="a"
1240+
v-for="a in items">
1241+
</div>
1242+
</template>`,
1243+
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }],
1244+
output: `
1245+
<template>
1246+
<div
1247+
v-for="a in items"
1248+
v-if="a"
1249+
attr="a">
1250+
</div>
1251+
</template>`,
1252+
errors: ['Attribute "v-for" should go before "v-if".']
1253+
},
1254+
{
1255+
filename: 'test.vue',
1256+
code: `
1257+
<template>
1258+
<div
1259+
attr="a"
1260+
v-if="a"
1261+
v-for="a in items">
1262+
</div>
1263+
</template>`,
1264+
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }],
1265+
output: `
1266+
<template>
1267+
<div
1268+
attr="a"
1269+
v-for="a in items"
1270+
v-if="a">
1271+
</div>
1272+
</template>`,
1273+
errors: ['Attribute "v-for" should go before "v-if".']
1274+
},
1275+
1276+
// slot
1277+
{
1278+
filename: 'test.vue',
1279+
options: [
1280+
{
1281+
order: [
1282+
'UNIQUE',
1283+
'LIST_RENDERING',
1284+
'CONDITIONALS',
1285+
'RENDER_MODIFIERS',
1286+
'GLOBAL',
1287+
'TWO_WAY_BINDING',
1288+
'OTHER_DIRECTIVES',
1289+
'OTHER_ATTR',
1290+
'EVENTS',
1291+
'CONTENT',
1292+
'DEFINITION',
1293+
'SLOT'
1294+
]
1295+
}
1296+
],
1297+
code:
1298+
'<template><div ref="foo" v-slot="{ qux }" bar="baz"></div></template>',
1299+
output:
1300+
'<template><div ref="foo" bar="baz" v-slot="{ qux }"></div></template>',
1301+
errors: [
1302+
{
1303+
message: 'Attribute "bar" should go before "v-slot".'
1304+
}
1305+
]
1306+
},
1307+
1308+
{
1309+
filename: 'test.vue',
1310+
options: [
1311+
{
1312+
order: [
1313+
'UNIQUE',
1314+
'LIST_RENDERING',
1315+
'CONDITIONALS',
1316+
'RENDER_MODIFIERS',
1317+
'GLOBAL',
1318+
'TWO_WAY_BINDING',
1319+
'OTHER_DIRECTIVES',
1320+
'OTHER_ATTR',
1321+
'EVENTS',
1322+
'CONTENT',
1323+
'DEFINITION',
1324+
'SLOT'
1325+
]
1326+
}
1327+
],
1328+
code:
1329+
'<template><div bar="baz" ref="foo" v-slot="{ qux }"></div></template>',
1330+
output:
1331+
'<template><div ref="foo" bar="baz" v-slot="{ qux }"></div></template>',
1332+
errors: [
1333+
{
1334+
message: 'Attribute "ref" should go before "bar".'
1335+
}
1336+
]
12161337
}
12171338
]
12181339
})

0 commit comments

Comments
 (0)