Skip to content

Commit 0b2e7fa

Browse files
committed
Change to use selector parser in component-tags-order rule.
1 parent c877991 commit 0b2e7fa

File tree

58 files changed

+256
-469
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+256
-469
lines changed

docs/rules/component-tags-order.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This rule warns about the order of the top-level tags, such as `<script>`, `<tem
2626
}
2727
```
2828

29-
- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`. May also be CSS selectors, such as `script[setup]` and `i18n:not([lang=en])`.
29+
- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`. May also be CSS selectors, such as `script[setup]` and `i18n:not([locale=en])`.
3030

3131
### `{ "order": [ [ "script", "template" ], "style" ] }` (default)
3232

@@ -161,26 +161,26 @@ This rule warns about the order of the top-level tags, such as `<script>`, `<tem
161161

162162
</eslint-code-block>
163163

164-
### `{ 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }`
164+
### `{ 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }`
165165

166-
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">
166+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }]}">
167167

168168
```vue
169169
<!-- ✓ GOOD -->
170170
<template>...</template>
171-
<i18n lang="ja">/* ... */</i18n>
172-
<i18n lang="en">/* ... */</i18n>
171+
<i18n locale="ja">/* ... */</i18n>
172+
<i18n locale="en">/* ... */</i18n>
173173
```
174174

175175
</eslint-code-block>
176176

177-
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">
177+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }]}">
178178

179179
```vue
180180
<!-- ✗ BAD -->
181181
<template>...</template>
182-
<i18n lang="en">/* ... */</i18n>
183-
<i18n lang="ja">/* ... */</i18n>
182+
<i18n locale="en">/* ... */</i18n>
183+
<i18n locale="ja">/* ... */</i18n>
184184
```
185185

186186
</eslint-code-block>

lib/rules/component-tags-order.js

Lines changed: 66 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@
99
// ------------------------------------------------------------------------------
1010

1111
const utils = require('../utils')
12-
const parser = require('postcss-selector-parser')
12+
const { parseSelector } = require('../utils/selector')
13+
14+
/**
15+
* @typedef {import('../utils/selector').VElementSelector} VElementSelector
16+
*/
17+
18+
// ------------------------------------------------------------------------------
19+
// Helpers
20+
// ------------------------------------------------------------------------------
1321

1422
const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
1523

@@ -47,32 +55,46 @@ module.exports = {
4755
],
4856
messages: {
4957
unexpected:
50-
'<{{elementName}}{{elementAttributes}}> should be above <{{firstUnorderedName}}{{firstUnorderedAttributes}}> on line {{line}}.'
58+
"'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}."
5159
}
5260
},
5361
/**
5462
* @param {RuleContext} context - The rule context.
5563
* @returns {RuleListener} AST event handlers.
5664
*/
5765
create(context) {
58-
/** @type {Map<string, number>} */
59-
const orderMap = new Map()
66+
/**
67+
* @typedef {object} OrderElement
68+
* @property {string} selectorText
69+
* @property {VElementSelector} selector
70+
* @property {number} index
71+
*/
72+
/** @type {OrderElement[]} */
73+
const orders = []
6074
/** @type {(string|string[])[]} */
6175
const orderOptions =
6276
(context.options[0] && context.options[0].order) || DEFAULT_ORDER
63-
orderOptions.forEach((nameOrNames, index) => {
64-
if (Array.isArray(nameOrNames)) {
65-
for (const name of nameOrNames) {
66-
orderMap.set(name, index)
77+
orderOptions.forEach((selectorOrSelectors, index) => {
78+
if (Array.isArray(selectorOrSelectors)) {
79+
for (const selector of selectorOrSelectors) {
80+
orders.push({
81+
selectorText: selector,
82+
selector: parseSelector(selector, context),
83+
index
84+
})
6785
}
6886
} else {
69-
orderMap.set(nameOrNames, index)
87+
orders.push({
88+
selectorText: selectorOrSelectors,
89+
selector: parseSelector(selectorOrSelectors, context),
90+
index
91+
})
7092
}
7193
})
7294

7395
/**
7496
* @param {VElement} element
75-
* @return {String}
97+
* @return {string}
7698
*/
7799
function getAttributeString(element) {
78100
return element.startTag.attributes
@@ -83,61 +105,18 @@ module.exports = {
83105

84106
return `${attribute.key.name}${
85107
attribute.value && attribute.value.value
86-
? '=' + attribute.value.value
108+
? `=${attribute.value.value}`
87109
: ''
88110
}`
89111
})
90112
.join(' ')
91113
}
92114

93-
/**
94-
* @param {String} ordering
95-
* @param {VElement} element
96-
* @return {Boolean} true if the element matches the selector, false otherwise
97-
*/
98-
function matches(ordering, element) {
99-
let attributeMatches = true
100-
let isNegated = false
101-
let tagMatches = true
102-
103-
parser((selectors) => {
104-
selectors.walk((selector) => {
105-
switch (selector.type) {
106-
case 'tag':
107-
tagMatches = selector.value === element.name
108-
break
109-
case 'pseudo':
110-
isNegated = selector.value === ':not'
111-
break
112-
case 'attribute':
113-
attributeMatches = utils.hasAttribute(
114-
element,
115-
selector.qualifiedAttribute,
116-
selector.value
117-
)
118-
break
119-
}
120-
})
121-
}).processSync(ordering)
122-
123-
if (isNegated) {
124-
return tagMatches && !attributeMatches
125-
} else {
126-
return tagMatches && attributeMatches
127-
}
128-
}
129-
130115
/**
131116
* @param {VElement} element
132117
*/
133-
function getOrderPosition(element) {
134-
for (const [ordering, index] of orderMap.entries()) {
135-
if (matches(ordering, element)) {
136-
return index
137-
}
138-
}
139-
140-
return -1
118+
function getOrderElement(element) {
119+
return orders.find((o) => o.selector.test(element))
141120
}
142121
const documentFragment =
143122
context.parserServices.getDocumentFragment &&
@@ -156,18 +135,34 @@ module.exports = {
156135
return
157136
}
158137
const elements = getTopLevelHTMLElements()
138+
139+
const elementWithOrders = elements
140+
.map((element) => {
141+
const order = getOrderElement(element)
142+
return {
143+
order,
144+
element
145+
}
146+
})
147+
.filter(
148+
/**
149+
* @param { {order:OrderElement|undefined, element: VElement} } o
150+
* @returns {o is {order:OrderElement, element: VElement}}
151+
*/
152+
(o) => Boolean(o.order)
153+
)
159154
const sourceCode = context.getSourceCode()
160-
elements.forEach((element, index) => {
161-
const expectedIndex = getOrderPosition(element)
162-
if (expectedIndex < 0) {
163-
return
164-
}
165-
const firstUnordered = elements
155+
elementWithOrders.forEach(({ order: expected, element }, index) => {
156+
const firstUnordered = elementWithOrders
166157
.slice(0, index)
167-
.filter((e) => expectedIndex < getOrderPosition(e))
168-
.sort((e1, e2) => getOrderPosition(e1) - getOrderPosition(e2))[0]
158+
.filter(({ order }) => {
159+
return expected.index < order.index
160+
})
161+
.sort((e1, e2) => e1.order.index - e2.order.index)[0]
169162
if (firstUnordered) {
170-
const firstUnorderedttributes = getAttributeString(firstUnordered)
163+
const firstUnorderedAttributes = getAttributeString(
164+
firstUnordered.element
165+
)
171166
const elementAttributes = getAttributeString(element)
172167

173168
context.report({
@@ -177,18 +172,18 @@ module.exports = {
177172
data: {
178173
elementName: element.name,
179174
elementAttributes: elementAttributes
180-
? ' ' + elementAttributes
175+
? ` ${elementAttributes}`
181176
: '',
182-
firstUnorderedName: firstUnordered.name,
183-
firstUnorderedAttributes: firstUnorderedttributes
184-
? ' ' + firstUnorderedttributes
177+
firstUnorderedName: firstUnordered.element.name,
178+
firstUnorderedAttributes: firstUnorderedAttributes
179+
? ` ${firstUnorderedAttributes}`
185180
: '',
186-
line: firstUnordered.loc.start.line
181+
line: firstUnordered.element.loc.start.line
187182
},
188183
*fix(fixer) {
189184
// insert element before firstUnordered
190185
const fixedElements = elements.flatMap((it) => {
191-
if (it === firstUnordered) {
186+
if (it === firstUnordered.element) {
192187
return [element, it]
193188
} else if (it === element) {
194189
return []

0 commit comments

Comments
 (0)