Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4a71322

Browse files
author
erindepew
committedJan 5, 2018
attribute order linting
1 parent 7faf1a4 commit 4a71322

File tree

5 files changed

+346
-1
lines changed

5 files changed

+346
-1
lines changed
 

‎README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
161161
| | [require-prop-types](./docs/rules/require-prop-types.md) | require type definitions in props |
162162
| :wrench: | [v-bind-style](./docs/rules/v-bind-style.md) | enforce `v-bind` directive style |
163163
| :wrench: | [v-on-style](./docs/rules/v-on-style.md) | enforce `v-on` directive style |
164-
164+
| :wrench: | [attribute-order](./docs/rules/attribute-order.md) | enforce ordering of attributes |
165165

166166
### Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
167167

‎docs/rules/attribute-order.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# enforce ordering of attributes (attribute-order)
2+
3+
## :book: Rule Details
4+
5+
This rule aims to enfore ordering of component attributes. The default order is specified in the [Vue styleguide](https://vuejs.org/v2/style-guide/#Element-attribute-order-recommended).
6+
7+
:+1: Examples of **correct** code`:
8+
9+
```html
10+
<div
11+
is="header"
12+
v-for="item in items"
13+
v-if="!visible"
14+
v-once id="uniqueID"
15+
ref="header"
16+
v-model="headerData"
17+
myProp="prop"
18+
@click="functionCall"
19+
v-text="textContent">
20+
</div>
21+
```
22+
23+
```html
24+
<div
25+
v-for="item in items"
26+
v-if="!visible"
27+
propOne="prop"
28+
propTwo="prop"
29+
propThree="prop"
30+
@click="functionCall"
31+
v-text="textContent">
32+
</div>
33+
```
34+
35+
```html
36+
<div
37+
propOne="prop"
38+
propTwo="prop"
39+
propThree="prop">
40+
</div>
41+
```
42+
43+
:-1: Examples of **incorrect** code`:
44+
45+
```html
46+
<div
47+
ref="header"
48+
v-for="item in items"
49+
v-once id="uniqueID"
50+
v-model="headerData"
51+
myProp="prop"
52+
v-if="!visible"
53+
is="header"
54+
@click="functionCall"
55+
v-text="textContent">
56+
</div>
57+
```
58+
59+
### `order`
60+
61+
Specify custom order of attribute groups
62+
63+
:+1: Examples of **correct** code with custom order`:
64+
65+
```html
66+
<!-- options: [{ order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'OTHER_ATTR', 'EVENTS', 'CONTENT', 'DEFINITION'] }] -->
67+
<div
68+
propOne="prop"
69+
propTwo="prop"
70+
is="header">
71+
</div>
72+
```
73+
74+
```html
75+
<!-- options: [{ order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }] -->
76+
<div
77+
ref="header"
78+
is="header"
79+
propOne="prop"
80+
propTwo="prop">
81+
</div>
82+
```
83+
84+
:-1: Examples of **incorrect** code with custom order`:
85+
86+
```html
87+
<!-- options: [{ order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }] -->
88+
<div
89+
ref="header"
90+
propOne="prop"
91+
is="header">
92+
</div>
93+
```

‎lib/recommended-rules.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
module.exports = {
77
"vue/attribute-hyphenation": "error",
8+
"vue/attribute-order": "off",
89
"vue/html-end-tags": "error",
910
"vue/html-indent": "error",
1011
"vue/html-quotes": "error",

‎lib/rules/attribute-order.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @fileoverview enforce alphabetical ordering of properties and prioritizing vue-specific attributes
3+
* @author Erin Depew
4+
*/
5+
'use strict'
6+
const utils = require('../utils')
7+
8+
// ------------------------------------------------------------------------------
9+
// Rule Definition
10+
// ------------------------------------------------------------------------------
11+
12+
function getPosition (attribute, attributeOrder) {
13+
const name = attribute.key.name
14+
if (name === 'is') {
15+
return attributeOrder.indexOf('DEFINITION')
16+
} else if (name === 'for') {
17+
return attributeOrder.indexOf('LIST_RENDERING')
18+
} else if (name === 'if' || name === 'else-if' || name === 'else' || name === 'show' || name === 'cloak') {
19+
return attributeOrder.indexOf('CONDITIONALS')
20+
} else if (name === 'pre' || name === 'once') {
21+
return attributeOrder.indexOf('RENDER_MODIFIERS')
22+
} else if (name === 'id') {
23+
return attributeOrder.indexOf('GLOBAL')
24+
} else if (name === 'ref' || name === 'key' || name === 'slot') {
25+
return attributeOrder.indexOf('UNIQUE')
26+
} else if (name === 'model') {
27+
return attributeOrder.indexOf('BINDING')
28+
} else if (name === 'on') {
29+
return attributeOrder.indexOf('EVENTS')
30+
} else if (name === 'html' || name === 'text') {
31+
return attributeOrder.indexOf('CONTENT')
32+
} else {
33+
return attributeOrder.indexOf('OTHER_ATTR')
34+
}
35+
}
36+
37+
function create (context) {
38+
const sourceCode = context.getSourceCode()
39+
let attributeOrder = ['DEFINITION', 'LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'OTHER_ATTR', 'EVENTS', 'CONTENT']
40+
if (context.options[0] && context.options[0].order) {
41+
attributeOrder = context.options[0].order
42+
}
43+
let currentPosition
44+
let previousNode
45+
46+
function reportIssue (node, previousNode) {
47+
const currentNode = sourceCode.getText(node.key)
48+
const prevNode = sourceCode.getText(previousNode.key)
49+
context.report({
50+
node: node.key,
51+
loc: node.loc,
52+
message: `Attribute ${currentNode} must go before ${prevNode}.`,
53+
data: {
54+
currentNode
55+
}
56+
})
57+
}
58+
59+
return utils.defineTemplateBodyVisitor(context, {
60+
'VStartTag' () {
61+
currentPosition = -1
62+
},
63+
'VAttribute' (node) {
64+
if (currentPosition === -1) {
65+
currentPosition = getPosition(node, attributeOrder)
66+
previousNode = node
67+
} else if (currentPosition <= getPosition(node, attributeOrder)) {
68+
currentPosition = getPosition(node, attributeOrder)
69+
previousNode = node
70+
} else {
71+
reportIssue(node, previousNode)
72+
}
73+
}
74+
})
75+
}
76+
77+
module.exports = {
78+
meta: {
79+
docs: {
80+
description: 'enforce alphabetical ordering of properties and prioritizing vue-specific attributes',
81+
category: 'Fill me in',
82+
recommended: false
83+
},
84+
fixable: null,
85+
schema: {
86+
type: 'array',
87+
properties: {
88+
order: {
89+
items: {
90+
type: 'string'
91+
},
92+
maxItems: 10,
93+
minItems: 10
94+
}
95+
}
96+
}
97+
},
98+
create
99+
}

‎tests/lib/rules/attribute-order.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* @fileoverview enforce alphabetical ordering of properties and prioritizing vue-specific attributes
3+
* @author Erin Depew
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
var rule = require('../../../lib/rules/attribute-order')
12+
var RuleTester = require('eslint').RuleTester
13+
14+
// ------------------------------------------------------------------------------
15+
// Tests
16+
// ------------------------------------------------------------------------------
17+
18+
var tester = new RuleTester({
19+
parser: 'vue-eslint-parser',
20+
parserOptions: { ecmaVersion: 2015 }
21+
})
22+
tester.run('attribute-order', rule, {
23+
24+
valid: [
25+
{
26+
filename: 'test.vue',
27+
code: '<template><div></div></template>'
28+
},
29+
{
30+
filename: 'test.vue',
31+
code: '<template><div is="header"></div></template>'
32+
},
33+
{
34+
filename: 'test.vue',
35+
code: '<template><div v-for="item in items"></div></template>'
36+
},
37+
{
38+
filename: 'test.vue',
39+
code: '<template><div v-if="!visible"></div></template>'
40+
},
41+
{
42+
filename: 'test.vue',
43+
code: '<template><div v-else-if="!visible"></div></template>'
44+
},
45+
{
46+
filename: 'test.vue',
47+
code: '<template><div v-else="!visible"></div></template>'
48+
},
49+
{
50+
filename: 'test.vue',
51+
code: '<template><div v-show="!visible"></div></template>'
52+
},
53+
{
54+
filename: 'test.vue',
55+
code: '<template><div v-cloak></div></template>'
56+
},
57+
{
58+
filename: 'test.vue',
59+
code: '<template><div v-pre></div></template>'
60+
},
61+
{
62+
filename: 'test.vue',
63+
code: '<template><div v-once></div></template>'
64+
},
65+
{
66+
filename: 'test.vue',
67+
code: '<template><div id="header"></div></template>'
68+
},
69+
{
70+
filename: 'test.vue',
71+
code: '<template><div key="id"></div></template>'
72+
},
73+
{
74+
filename: 'test.vue',
75+
code: '<template><div v-html="htmlContent"></div></template>'
76+
},
77+
{
78+
filename: 'test.vue',
79+
code: '<template><div v-text="textContent"></div></template>'
80+
},
81+
{
82+
filename: 'test.vue',
83+
code: '<template><div v-model="toggle"></div></template>'
84+
},
85+
{
86+
filename: 'test.vue',
87+
code: '<template><div @click="functionCall"></div></template>'
88+
},
89+
{
90+
filename: 'test.vue',
91+
code: '<template><div myProp="prop"></div></template>'
92+
},
93+
{
94+
filename: 'test.vue',
95+
code: '<template><div is="header" v-for="item in items" v-if="!visible" v-once id="uniqueID" ref="header" v-model="headerData" myProp="prop" @click="functionCall" v-text="textContent"></div></template>'
96+
},
97+
{
98+
filename: 'test.vue',
99+
code: '<template><div v-for="item in items" v-if="!visible" propone="prop" proptwo="prop" propthree="prop" @click="functionCall" v-text="textContent"></div></template>'
100+
},
101+
{
102+
filename: 'test.vue',
103+
code: '<template><div propone="prop" proptwo="prop" propthree="prop"></div></template>'
104+
},
105+
{
106+
filename: 'test.vue',
107+
code: '<template><div propone="prop" proptwo="prop" is="header"></div></template>',
108+
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'OTHER_ATTR', 'EVENTS', 'CONTENT', 'DEFINITION'] }]
109+
},
110+
{
111+
filename: 'test.vue',
112+
code: '<template><div ref="header" is="header" propone="prop" proptwo="prop"></div></template>',
113+
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }]
114+
}
115+
],
116+
117+
invalid: [
118+
{
119+
filename: 'test.vue',
120+
code: '<template><div v-cloak is="header"></div></template>',
121+
errors: [{
122+
message: 'Attribute is must go before v-cloak.',
123+
type: 'VIdentifier'
124+
}]
125+
},
126+
{
127+
filename: 'test.vue',
128+
code: '<template><div id="uniqueID" v-cloak></div></template>',
129+
errors: [{
130+
message: 'Attribute v-cloak must go before id.',
131+
type: 'VDirectiveKey'
132+
}]
133+
},
134+
{
135+
filename: 'test.vue',
136+
code: '<template><div data-id="foo" aria-test="bar" is="custom" myProp="prop"></div></template>',
137+
errors: [{
138+
message: 'Attribute is must go before aria-test.',
139+
type: 'VIdentifier'
140+
}]
141+
},
142+
{
143+
filename: 'test.vue',
144+
code: '<template><div ref="header" propone="prop" is="header" ></div></template>',
145+
options: [{ order: ['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'BINDING', 'DEFINITION', 'OTHER_ATTR', 'EVENTS', 'CONTENT'] }],
146+
errors: [{
147+
message: 'Attribute is must go before propone.',
148+
type: 'VIdentifier'
149+
}]
150+
}
151+
]
152+
})

0 commit comments

Comments
 (0)
Please sign in to comment.