Skip to content

Commit 31666df

Browse files
committed
Add block-attributes-order rule
1 parent 6173b91 commit 31666df

File tree

4 files changed

+556
-4
lines changed

4 files changed

+556
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ yarn.lock
99
yarn-error.log
1010
docs/.vuepress/dist
1111
typings/eslint/lib/rules
12+
.DS_Store

lib/rules/attributes-order.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -382,10 +382,10 @@ module.exports = {
382382
{
383383
type: 'array',
384384
items: {
385-
enum: Object.values(ATTRS),
386-
uniqueItems: true,
387-
additionalItems: false
388-
}
385+
enum: Object.values(ATTRS)
386+
},
387+
uniqueItems: true,
388+
additionalItems: false
389389
}
390390
]
391391
},

lib/rules/block-attributes-order.js

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/**
2+
* @fileoverview enforce ordering of block attributes
3+
* @author Wenlu Wang
4+
*/
5+
'use strict'
6+
const utils = require('../utils')
7+
8+
/**
9+
* @enum {string}
10+
*/
11+
const WELL_KNOWN_TEMPLATE_ATTRS = {
12+
functional: 'functional',
13+
lang: 'lang',
14+
src: 'src'
15+
}
16+
17+
/**
18+
* @enum {string}
19+
*/
20+
const WELL_KNOWN_SCRIPT_ATTRS = {
21+
lang: 'lang',
22+
setup: 'setup',
23+
src: 'src'
24+
}
25+
26+
/**
27+
* @enum {string}
28+
*/
29+
const WELL_KNOWN_STYLE_ATTRS = {
30+
scoped: 'scoped',
31+
module: 'module',
32+
lang: 'lang',
33+
src: 'src'
34+
}
35+
36+
/**
37+
* @template {string} T
38+
* @typedef {T | T[]} OrderItem
39+
*/
40+
41+
/**
42+
* @typedef WellKnownOrders
43+
* @property { OrderItem<WELL_KNOWN_TEMPLATE_ATTRS>[] } [template]
44+
* @property { OrderItem<WELL_KNOWN_SCRIPT_ATTRS>[] } [script]
45+
* @property { OrderItem<WELL_KNOWN_STYLE_ATTRS>[] } [style]
46+
*/
47+
48+
/**
49+
* @typedef UserOptions
50+
* @property {WellKnownOrders & Record<string, OrderItem<string>[]>} [order]
51+
*/
52+
53+
/**
54+
* Normalizes a given options.
55+
* @param {UserOptions?} options An option to parse.
56+
* @return {WellKnownOrders & Record<string, OrderItem<string>[]>}
57+
*/
58+
function normalizeOptions(options) {
59+
if (!options || !options.order) {
60+
return {
61+
template: [
62+
WELL_KNOWN_TEMPLATE_ATTRS.functional,
63+
WELL_KNOWN_TEMPLATE_ATTRS.lang,
64+
WELL_KNOWN_TEMPLATE_ATTRS.src
65+
],
66+
script: [
67+
WELL_KNOWN_SCRIPT_ATTRS.lang,
68+
WELL_KNOWN_SCRIPT_ATTRS.setup,
69+
WELL_KNOWN_SCRIPT_ATTRS.src
70+
],
71+
style: [
72+
WELL_KNOWN_STYLE_ATTRS.lang,
73+
WELL_KNOWN_STYLE_ATTRS.module,
74+
WELL_KNOWN_STYLE_ATTRS.scoped,
75+
WELL_KNOWN_STYLE_ATTRS.src
76+
]
77+
}
78+
}
79+
return options.order
80+
}
81+
82+
/**
83+
* @param {WellKnownOrders & Record<string, OrderItem<string>[]>} order
84+
*/
85+
function normalizeAttributePositions(order) {
86+
/**
87+
* @type { Record<string, Record<string, number>> }
88+
*/
89+
const attributePositions = {}
90+
for (const [blockName, blockOrder] of Object.entries(order)) {
91+
/**
92+
* @type { Record<string, number> }
93+
*/
94+
const attributePosition = {}
95+
for (const [i, o] of blockOrder.entries()) {
96+
if (Array.isArray(o)) {
97+
for (const attr of o) {
98+
attributePosition[attr] = i
99+
}
100+
} else {
101+
attributePosition[o] = i
102+
}
103+
}
104+
attributePositions[blockName] = attributePosition
105+
}
106+
return attributePositions
107+
}
108+
109+
/**
110+
* @param {VAttribute | VDirective} attribute
111+
* @param { Record<string, number> } attributePosition
112+
* @returns {number | null}
113+
*/
114+
function getPosition(attribute, attributePosition) {
115+
if (attribute.directive) {
116+
return null
117+
}
118+
119+
return attributePosition[attribute.key.name]
120+
}
121+
122+
/**
123+
* @param {VStartTag} node
124+
* @param {Record<string, number>} attributePosition
125+
*/
126+
function getAttributeAndPositionList(node, attributePosition) {
127+
/**
128+
* @type {{ attr: (VAttribute | VDirective), position: number }[]}
129+
*/
130+
const results = []
131+
for (const attr of node.attributes) {
132+
const position = getPosition(attr, attributePosition)
133+
if (position == null) {
134+
continue
135+
}
136+
results.push({ attr, position })
137+
}
138+
return results
139+
}
140+
141+
/**
142+
* @param {RuleContext} context - The rule context.
143+
* @returns {RuleListener} AST event handlers.
144+
*/
145+
function create(context) {
146+
const sourceCode = context.getSourceCode()
147+
const order = normalizeOptions(context.options[0])
148+
const attributeAndPositions = normalizeAttributePositions(order)
149+
150+
/**
151+
* @param {VAttribute | VDirective} node
152+
* @param {VAttribute | VDirective} previousNode
153+
*/
154+
function reportIssue(node, previousNode) {
155+
const currentNodeText = sourceCode.getText(node.key)
156+
const prevNode = sourceCode.getText(previousNode.key)
157+
158+
/**
159+
* @param {RuleFixer} fixer
160+
*/
161+
function fix(fixer) {
162+
const attributes = node.parent.attributes
163+
164+
const previousNodes = attributes.slice(
165+
attributes.indexOf(previousNode),
166+
attributes.indexOf(node)
167+
)
168+
const moveNodes = [node]
169+
for (const n of previousNodes) {
170+
moveNodes.push(n)
171+
}
172+
173+
return moveNodes.map((moveNode, index) => {
174+
const text = sourceCode.getText(moveNode)
175+
return fixer.replaceText(previousNodes[index] || node, text)
176+
})
177+
}
178+
179+
context.report({
180+
node,
181+
message: `Attribute "${currentNodeText}" should go before "${prevNode}".`,
182+
data: {
183+
currentNodeText
184+
},
185+
fix
186+
})
187+
}
188+
189+
/**
190+
* @param {VElement} element
191+
* @returns {void}
192+
*/
193+
function verify(element) {
194+
const tag = element.name
195+
const attributePosition = attributeAndPositions[tag]
196+
if (!attributePosition) {
197+
return
198+
}
199+
200+
const attributeAndPositionList = getAttributeAndPositionList(
201+
element.startTag,
202+
attributePosition
203+
)
204+
if (attributeAndPositionList.length <= 1) {
205+
return
206+
}
207+
208+
let { attr: previousNode, position: previousPosition } =
209+
attributeAndPositionList[0]
210+
for (let index = 1; index < attributeAndPositionList.length; index++) {
211+
const { attr, position } = attributeAndPositionList[index]
212+
if (previousPosition <= position) {
213+
previousNode = attr
214+
previousPosition = position
215+
} else {
216+
reportIssue(attr, previousNode)
217+
}
218+
}
219+
}
220+
221+
return utils.defineDocumentVisitor(context, {
222+
'VDocumentFragment > VElement': verify
223+
})
224+
}
225+
226+
module.exports = {
227+
meta: {
228+
type: 'suggestion',
229+
docs: {
230+
description: 'enforce order of block attributes',
231+
categories: undefined,
232+
url: 'https://eslint.vuejs.org/rules/block-attributes-order.html'
233+
},
234+
fixable: 'code',
235+
schema: [
236+
{
237+
type: 'object',
238+
properties: {
239+
order: {
240+
type: 'object',
241+
properties: {
242+
template: {
243+
type: 'array',
244+
items: {
245+
anyOf: [
246+
{ enum: Object.values(WELL_KNOWN_TEMPLATE_ATTRS) },
247+
{ type: 'string' },
248+
{
249+
type: 'array',
250+
items: {
251+
anyOf: [
252+
{ enum: Object.values(WELL_KNOWN_TEMPLATE_ATTRS) },
253+
{ type: 'string' }
254+
]
255+
},
256+
uniqueItems: true
257+
}
258+
]
259+
},
260+
uniqueItems: true
261+
},
262+
script: {
263+
type: 'array',
264+
items: {
265+
anyOf: [
266+
{ enum: Object.values(WELL_KNOWN_SCRIPT_ATTRS) },
267+
{ type: 'string' },
268+
{
269+
type: 'array',
270+
items: {
271+
anyOf: [
272+
{ enum: Object.values(WELL_KNOWN_SCRIPT_ATTRS) },
273+
{ type: 'string' }
274+
]
275+
},
276+
uniqueItems: true
277+
}
278+
]
279+
},
280+
uniqueItems: true
281+
},
282+
style: {
283+
type: 'array',
284+
items: {
285+
anyOf: [
286+
{ enum: Object.values(WELL_KNOWN_STYLE_ATTRS) },
287+
{ type: 'string' },
288+
{
289+
type: 'array',
290+
items: {
291+
anyOf: [
292+
{ enum: Object.values(WELL_KNOWN_STYLE_ATTRS) },
293+
{ type: 'string' }
294+
]
295+
},
296+
uniqueItems: true
297+
}
298+
]
299+
},
300+
uniqueItems: true
301+
}
302+
},
303+
additionalProperties: {
304+
type: 'array',
305+
items: {
306+
type: 'string'
307+
},
308+
uniqueItems: true
309+
}
310+
}
311+
},
312+
additionalProperties: false
313+
}
314+
]
315+
},
316+
create
317+
}

0 commit comments

Comments
 (0)