Skip to content

Commit

Permalink
Improve usage of postcss-selector-parser (#1842)
Browse files Browse the repository at this point in the history
* Add CSS selector parser

* Change to use selector parser in `component-tags-order` rule.

* Update tests/lib/utils/selector.js

Co-authored-by: Flo Edelmann <florian-edelmann@online.de>

* Update fixtures

* Update lib/rules/component-tags-order.js

Co-authored-by: Flo Edelmann <florian-edelmann@online.de>

* Update lib/rules/component-tags-order.js

Co-authored-by: Flo Edelmann <florian-edelmann@online.de>

* Update lib/utils/selector.js

Co-authored-by: Flo Edelmann <florian-edelmann@online.de>

* Update tests/lib/utils/selector.js

Co-authored-by: Flo Edelmann <florian-edelmann@online.de>

* format and update fixtures

* Refactor

* rename var

Co-authored-by: Flo Edelmann <florian-edelmann@online.de>
  • Loading branch information
ota-meshi and FloEdelmann committed Apr 12, 2022
1 parent 124cc37 commit 1e375bc
Show file tree
Hide file tree
Showing 136 changed files with 2,365 additions and 82 deletions.
16 changes: 8 additions & 8 deletions docs/rules/component-tags-order.md
Expand Up @@ -26,7 +26,7 @@ This rule warns about the order of the top-level tags, such as `<script>`, `<tem
}
```

- `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])`.
- `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])`.

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

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

</eslint-code-block>

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

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

```vue
<!-- ✓ GOOD -->
<template>...</template>
<i18n lang="ja">/* ... */</i18n>
<i18n lang="en">/* ... */</i18n>
<i18n locale="ja">/* ... */</i18n>
<i18n locale="en">/* ... */</i18n>
```

</eslint-code-block>

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

```vue
<!-- ✗ BAD -->
<template>...</template>
<i18n lang="en">/* ... */</i18n>
<i18n lang="ja">/* ... */</i18n>
<i18n locale="en">/* ... */</i18n>
<i18n locale="ja">/* ... */</i18n>
```

</eslint-code-block>
Expand Down
118 changes: 50 additions & 68 deletions lib/rules/component-tags-order.js
Expand Up @@ -9,7 +9,15 @@
// ------------------------------------------------------------------------------

const utils = require('../utils')
const parser = require('postcss-selector-parser')
const { parseSelector } = require('../utils/selector')

/**
* @typedef {import('../utils/selector').VElementSelector} VElementSelector
*/

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

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

Expand Down Expand Up @@ -55,24 +63,38 @@ module.exports = {
* @returns {RuleListener} AST event handlers.
*/
create(context) {
/** @type {Map<string, number>} */
const orderMap = new Map()
/**
* @typedef {object} OrderElement
* @property {string} selectorText
* @property {VElementSelector} selector
* @property {number} index
*/
/** @type {OrderElement[]} */
const orders = []
/** @type {(string|string[])[]} */
const orderOptions =
(context.options[0] && context.options[0].order) || DEFAULT_ORDER
orderOptions.forEach((nameOrNames, index) => {
if (Array.isArray(nameOrNames)) {
for (const name of nameOrNames) {
orderMap.set(name, index)
orderOptions.forEach((selectorOrSelectors, index) => {
if (Array.isArray(selectorOrSelectors)) {
for (const selector of selectorOrSelectors) {
orders.push({
selectorText: selector,
selector: parseSelector(selector, context),
index
})
}
} else {
orderMap.set(nameOrNames, index)
orders.push({
selectorText: selectorOrSelectors,
selector: parseSelector(selectorOrSelectors, context),
index
})
}
})

/**
* @param {VElement} element
* @return {String}
* @return {string}
*/
function getAttributeString(element) {
return element.startTag.attributes
Expand All @@ -90,54 +112,11 @@ module.exports = {
.join(' ')
}

/**
* @param {String} ordering
* @param {VElement} element
* @return {Boolean} true if the element matches the selector, false otherwise
*/
function matches(ordering, element) {
let attributeMatches = true
let isNegated = false
let tagMatches = true

parser((selectors) => {
selectors.walk((selector) => {
switch (selector.type) {
case 'tag':
tagMatches = selector.value === element.name
break
case 'pseudo':
isNegated = selector.value === ':not'
break
case 'attribute':
attributeMatches = utils.hasAttribute(
element,
selector.qualifiedAttribute,
selector.value
)
break
}
})
}).processSync(ordering)

if (isNegated) {
return tagMatches && !attributeMatches
} else {
return tagMatches && attributeMatches
}
}

/**
* @param {VElement} element
*/
function getOrderPosition(element) {
for (const [ordering, index] of orderMap.entries()) {
if (matches(ordering, element)) {
return index
}
}

return -1
function getOrderElement(element) {
return orders.find((o) => o.selector.test(element))
}
const documentFragment =
context.parserServices.getDocumentFragment &&
Expand All @@ -156,18 +135,21 @@ module.exports = {
return
}
const elements = getTopLevelHTMLElements()

const elementWithOrders = elements.flatMap((element) => {
const order = getOrderElement(element)
return order ? [{ order, element }] : []
})
const sourceCode = context.getSourceCode()
elements.forEach((element, index) => {
const expectedIndex = getOrderPosition(element)
if (expectedIndex < 0) {
return
}
const firstUnordered = elements
elementWithOrders.forEach(({ order: expected, element }, index) => {
const firstUnordered = elementWithOrders
.slice(0, index)
.filter((e) => expectedIndex < getOrderPosition(e))
.sort((e1, e2) => getOrderPosition(e1) - getOrderPosition(e2))[0]
.filter(({ order }) => expected.index < order.index)
.sort((e1, e2) => e1.order.index - e2.order.index)[0]
if (firstUnordered) {
const firstUnorderedttributes = getAttributeString(firstUnordered)
const firstUnorderedAttributes = getAttributeString(
firstUnordered.element
)
const elementAttributes = getAttributeString(element)

context.report({
Expand All @@ -179,16 +161,16 @@ module.exports = {
elementAttributes: elementAttributes
? ` ${elementAttributes}`
: '',
firstUnorderedName: firstUnordered.name,
firstUnorderedAttributes: firstUnorderedttributes
? ` ${firstUnorderedttributes}`
firstUnorderedName: firstUnordered.element.name,
firstUnorderedAttributes: firstUnorderedAttributes
? ` ${firstUnorderedAttributes}`
: '',
line: firstUnordered.loc.start.line
line: firstUnordered.element.loc.start.line
},
*fix(fixer) {
// insert element before firstUnordered
const fixedElements = elements.flatMap((it) => {
if (it === firstUnordered) {
if (it === firstUnordered.element) {
return [element, it]
} else if (it === element) {
return []
Expand Down

0 comments on commit 1e375bc

Please sign in to comment.