Skip to content

Commit

Permalink
Add "SLOT" option to vue/attributes-order rule to specify v-slot orde…
Browse files Browse the repository at this point in the history
…r. (#1429)

* feat(attribute-order): add slot attribute

* update

* revert option schema

Co-authored-by: Stas Lashmanov <stasvarenkin@gmail.com>
  • Loading branch information
ota-meshi and CyberAP committed Feb 4, 2021
1 parent cc9c140 commit 7965d12
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 57 deletions.
6 changes: 4 additions & 2 deletions docs/rules/attributes-order.md
Expand Up @@ -27,7 +27,9 @@ This rule aims to enforce ordering of component attributes. The default order is
- `GLOBAL`
e.g. 'id'
- `UNIQUE`
e.g. 'ref', 'key', 'v-slot', 'slot'
e.g. 'ref', 'key'
- `SLOT`
e.g. 'v-slot', 'slot'.
- `TWO_WAY_BINDING`
e.g. 'v-model'
- `OTHER_DIRECTIVES`
Expand Down Expand Up @@ -127,7 +129,7 @@ Note that `v-bind="object"` syntax is considered to be the same as the next or p
"CONDITIONALS",
"RENDER_MODIFIERS",
"GLOBAL",
"UNIQUE",
["UNIQUE", "SLOT"],
"TWO_WAY_BINDING",
"OTHER_DIRECTIVES",
"OTHER_ATTR",
Expand Down
131 changes: 76 additions & 55 deletions lib/rules/attributes-order.js
Expand Up @@ -20,6 +20,7 @@ const ATTRS = {
RENDER_MODIFIERS: 'RENDER_MODIFIERS',
GLOBAL: 'GLOBAL',
UNIQUE: 'UNIQUE',
SLOT: 'SLOT',
TWO_WAY_BINDING: 'TWO_WAY_BINDING',
OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
OTHER_ATTR: 'OTHER_ATTR',
Expand Down Expand Up @@ -121,7 +122,7 @@ function getAttributeType(attribute) {
} else if (name === 'html' || name === 'text') {
return ATTRS.CONTENT
} else if (name === 'slot') {
return ATTRS.UNIQUE
return ATTRS.SLOT
} else if (name === 'is') {
return ATTRS.DEFINITION
} else {
Expand All @@ -139,13 +140,10 @@ function getAttributeType(attribute) {
return ATTRS.DEFINITION
} else if (propName === 'id') {
return ATTRS.GLOBAL
} else if (
propName === 'ref' ||
propName === 'key' ||
propName === 'slot' ||
propName === 'slot-scope'
) {
} else if (propName === 'ref' || propName === 'key') {
return ATTRS.UNIQUE
} else if (propName === 'slot' || propName === 'slot-scope') {
return ATTRS.SLOT
} else {
return ATTRS.OTHER_ATTR
}
Expand All @@ -154,12 +152,13 @@ function getAttributeType(attribute) {
/**
* @param {VAttribute | VDirective} attribute
* @param { { [key: string]: number } } attributePosition
* @returns {number | null} If the value is null, the order is omitted. Do not force the order.
*/
function getPosition(attribute, attributePosition) {
const attributeType = getAttributeType(attribute)
return attributePosition[attributeType] != null
? attributePosition[attributeType]
: -1
: null
}

/**
Expand Down Expand Up @@ -190,7 +189,7 @@ function create(context) {
ATTRS.CONDITIONALS,
ATTRS.RENDER_MODIFIERS,
ATTRS.GLOBAL,
ATTRS.UNIQUE,
[ATTRS.UNIQUE, ATTRS.SLOT],
ATTRS.TWO_WAY_BINDING,
ATTRS.OTHER_DIRECTIVES,
ATTRS.OTHER_ATTR,
Expand Down Expand Up @@ -267,74 +266,96 @@ function create(context) {

return utils.defineTemplateBodyVisitor(context, {
VStartTag(node) {
const attributes = node.attributes.filter((node, index, attributes) => {
if (
isVBindObject(node) &&
(isVAttributeOrVBind(attributes[index - 1]) ||
isVAttributeOrVBind(attributes[index + 1]))
) {
// In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax
// as they behave differently if you change the order.
return false
}
return true
})
if (attributes.length <= 1) {
const attributeAndPositions = getAttributeAndPositionList(node)
if (attributeAndPositions.length <= 1) {
return
}

let previousNode = attributes[0]
let previousPosition = getPositionFromAttrIndex(0)
for (let index = 1; index < attributes.length; index++) {
const node = attributes[index]
const position = getPositionFromAttrIndex(index)
let {
attr: previousNode,
position: previousPosition
} = attributeAndPositions[0]
for (let index = 1; index < attributeAndPositions.length; index++) {
const { attr, position } = attributeAndPositions[index]

let valid = previousPosition <= position
if (valid && alphabetical && previousPosition === position) {
valid = isAlphabetical(previousNode, node, sourceCode)
valid = isAlphabetical(previousNode, attr, sourceCode)
}
if (valid) {
previousNode = node
previousNode = attr
previousPosition = position
} else {
reportIssue(node, previousNode)
reportIssue(attr, previousNode)
}
}
}
})

/**
* @param {number} index
* @returns {number}
*/
function getPositionFromAttrIndex(index) {
const node = attributes[index]
if (isVBindObject(node)) {
// node is `v-bind ="object"` syntax
/**
* @param {VStartTag} node
* @returns { { attr: ( VAttribute | VDirective ), position: number }[] }
*/
function getAttributeAndPositionList(node) {
const attributes = node.attributes.filter((node, index, attributes) => {
if (
isVBindObject(node) &&
(isVAttributeOrVBind(attributes[index - 1]) ||
isVAttributeOrVBind(attributes[index + 1]))
) {
// In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax
// as they behave differently if you change the order.
return false
}
return true
})

// In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`,
// the behavior will be different, so adjust so that there is no change in behavior.
const results = []
for (let index = 0; index < attributes.length; index++) {
const attr = attributes[index]
const position = getPositionFromAttrIndex(index)
if (position == null) {
// The omitted order is skipped.
continue
}
results.push({ attr, position })
}

return results

/**
* @param {number} index
* @returns {number | null}
*/
function getPositionFromAttrIndex(index) {
const node = attributes[index]
if (isVBindObject(node)) {
// node is `v-bind ="object"` syntax

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

if (isVAttributeOrVBind(next) && !isVBindObject(next)) {
// It is considered to be in the same order as the next bind prop node.
return getPositionFromAttrIndex(nextIndex)
}
const len = attributes.length
for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
const next = attributes[nextIndex]

if (isVAttributeOrVBind(next) && !isVBindObject(next)) {
// It is considered to be in the same order as the next bind prop node.
return getPositionFromAttrIndex(nextIndex)
}
for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) {
const prev = attributes[prevIndex]
}
for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) {
const prev = attributes[prevIndex]

if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) {
// It is considered to be in the same order as the prev bind prop node.
return getPositionFromAttrIndex(prevIndex)
}
if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) {
// It is considered to be in the same order as the prev bind prop node.
return getPositionFromAttrIndex(prevIndex)
}
}
return getPosition(node, attributePosition)
}
return getPosition(node, attributePosition)
}
})
}
}

module.exports = {
Expand Down
121 changes: 121 additions & 0 deletions tests/lib/rules/attributes-order.js
Expand Up @@ -421,6 +421,20 @@ tester.run('attributes-order', rule, {
</div>
</template>`,
options: [{ alphabetical: true }]
},

// omit order
{
filename: 'test.vue',
code: `
<template>
<div
v-for="a in items"
v-if="a"
attr="a">
</div>
</template>`,
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }]
}
],

Expand Down Expand Up @@ -1213,6 +1227,113 @@ tester.run('attributes-order', rule, {
'Attribute "v-bind" should go before "v-on:click".',
'Attribute "v-if" should go before "v-on:click".'
]
},

// omit order
{
filename: 'test.vue',
code: `
<template>
<div
v-if="a"
attr="a"
v-for="a in items">
</div>
</template>`,
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }],
output: `
<template>
<div
v-for="a in items"
v-if="a"
attr="a">
</div>
</template>`,
errors: ['Attribute "v-for" should go before "v-if".']
},
{
filename: 'test.vue',
code: `
<template>
<div
attr="a"
v-if="a"
v-for="a in items">
</div>
</template>`,
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }],
output: `
<template>
<div
attr="a"
v-for="a in items"
v-if="a">
</div>
</template>`,
errors: ['Attribute "v-for" should go before "v-if".']
},

// slot
{
filename: 'test.vue',
options: [
{
order: [
'UNIQUE',
'LIST_RENDERING',
'CONDITIONALS',
'RENDER_MODIFIERS',
'GLOBAL',
'TWO_WAY_BINDING',
'OTHER_DIRECTIVES',
'OTHER_ATTR',
'EVENTS',
'CONTENT',
'DEFINITION',
'SLOT'
]
}
],
code:
'<template><div ref="foo" v-slot="{ qux }" bar="baz"></div></template>',
output:
'<template><div ref="foo" bar="baz" v-slot="{ qux }"></div></template>',
errors: [
{
message: 'Attribute "bar" should go before "v-slot".'
}
]
},

{
filename: 'test.vue',
options: [
{
order: [
'UNIQUE',
'LIST_RENDERING',
'CONDITIONALS',
'RENDER_MODIFIERS',
'GLOBAL',
'TWO_WAY_BINDING',
'OTHER_DIRECTIVES',
'OTHER_ATTR',
'EVENTS',
'CONTENT',
'DEFINITION',
'SLOT'
]
}
],
code:
'<template><div bar="baz" ref="foo" v-slot="{ qux }"></div></template>',
output:
'<template><div ref="foo" bar="baz" v-slot="{ qux }"></div></template>',
errors: [
{
message: 'Attribute "ref" should go before "bar".'
}
]
}
]
})

0 comments on commit 7965d12

Please sign in to comment.