diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md
index 9b053ab21..cc4aebbfa 100644
--- a/docs/rules/component-tags-order.md
+++ b/docs/rules/component-tags-order.md
@@ -14,7 +14,7 @@ since: v6.1.0
## :book: Rule Details
-This rule warns about the order of the `
+
+```
+
+
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+### `{ 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }`
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+
+
+```vue
+
+...
+
+
+```
+
+
+
+### `{ 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }`
+
+
+
+```vue
+
+...
+/* ... */
+/* ... */
+```
+
+
+
+
+
+```vue
+
+...
+/* ... */
+/* ... */
+```
+
+
+
## :books: Further Reading
- [Style guide - Single-file component top-level element order](https://vuejs.org/style-guide/rules-recommended.html#single-file-component-top-level-element-order)
diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js
index afb624f9f..289bc381f 100644
--- a/lib/rules/component-tags-order.js
+++ b/lib/rules/component-tags-order.js
@@ -9,6 +9,7 @@
// ------------------------------------------------------------------------------
const utils = require('../utils')
+const parser = require('postcss-selector-parser')
const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
@@ -46,7 +47,7 @@ module.exports = {
],
messages: {
unexpected:
- 'The <{{name}}> should be above the <{{firstUnorderedName}}> on line {{line}}.'
+ '<{{elementName}}{{elementAttributes}}> should be above <{{firstUnorderedName}}{{firstUnorderedAttributes}}> on line {{line}}.'
}
},
/**
@@ -70,11 +71,73 @@ module.exports = {
})
/**
- * @param {string} name
+ * @param {VElement} element
+ * @return {String}
*/
- function getOrderPosition(name) {
- const num = orderMap.get(name)
- return num == null ? -1 : num
+ function getAttributeString(element) {
+ return element.startTag.attributes
+ .map((attribute) => {
+ if (attribute.value && attribute.value.type !== 'VLiteral') {
+ return ''
+ }
+
+ return `${attribute.key.name}${
+ attribute.value && attribute.value.value
+ ? '=' + attribute.value.value
+ : ''
+ }`
+ })
+ .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
}
const documentFragment =
context.parserServices.getDocumentFragment &&
@@ -95,24 +158,31 @@ module.exports = {
const elements = getTopLevelHTMLElements()
const sourceCode = context.getSourceCode()
elements.forEach((element, index) => {
- const expectedIndex = getOrderPosition(element.name)
+ const expectedIndex = getOrderPosition(element)
if (expectedIndex < 0) {
return
}
const firstUnordered = elements
.slice(0, index)
- .filter((e) => expectedIndex < getOrderPosition(e.name))
- .sort(
- (e1, e2) => getOrderPosition(e1.name) - getOrderPosition(e2.name)
- )[0]
+ .filter((e) => expectedIndex < getOrderPosition(e))
+ .sort((e1, e2) => getOrderPosition(e1) - getOrderPosition(e2))[0]
if (firstUnordered) {
+ const firstUnorderedttributes = getAttributeString(firstUnordered)
+ const elementAttributes = getAttributeString(element)
+
context.report({
node: element,
loc: element.loc,
messageId: 'unexpected',
data: {
- name: element.name,
+ elementName: element.name,
+ elementAttributes: elementAttributes
+ ? ' ' + elementAttributes
+ : '',
firstUnorderedName: firstUnordered.name,
+ firstUnorderedAttributes: firstUnorderedttributes
+ ? ' ' + firstUnorderedttributes
+ : '',
line: firstUnordered.loc.start.line
},
*fix(fixer) {
diff --git a/package.json b/package.json
index 7abecb3f1..fbe19064b 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"dependencies": {
"eslint-utils": "^3.0.0",
"natural-compare": "^1.4.0",
+ "postcss-selector-parser": "^6.0.9",
"semver": "^7.3.5",
"vue-eslint-parser": "^8.0.1"
},
diff --git a/tests/lib/rules/component-tags-order.js b/tests/lib/rules/component-tags-order.js
index 1992d6fd2..84c2daeb6 100644
--- a/tests/lib/rules/component-tags-order.js
+++ b/tests/lib/rules/component-tags-order.js
@@ -50,6 +50,8 @@ tester.run('component-tags-order', rule, {
'',
'',
'',
+ '',
+ '',
`
@@ -102,11 +104,90 @@ tester.run('component-tags-order', rule, {
output: null,
options: [{ order: ['docs', 'script', 'template', 'style'] }]
},
+ {
+ code: '',
+ output: null,
+ options: [{ order: ['script[setup]', 'script:not([setup])', 'template', 'style'] }]
+ },
+ {
+ code: '',
+ output: null,
+ options: [{ order: [['script[setup]', 'script:not([setup])', 'template'], 'style'] }]
+ },
+ {
+ code: '',
+ output: null,
+ options: [{ order: ['script', 'template', 'style'] }]
+ },
+ {
+ code: '',
+ output: null,
+ options: [{ order: [['script, 'template'], 'style'] }]
+ },
+ {
+ code: '',
+ output: null,
+ options: [
+ { order: ['script:not([setup])', 'script[setup]', 'template', 'style'] }
+ ]
+ },
+ {
+ code: '',
+ output: null,
+ options: [
+ {
+ order: [['script:not([setup])', 'script[setup]', 'template'], 'style']
+ }
+ ]
+ },
+ {
+ code: '',
+ output: null,
+ options: [
+ {
+ order: [
+ ['script:not([setup])', 'script[setup]', 'template'],
+ 'style[scoped]',
+ 'style:not([scoped])',
+ 'i18n:not([lang=en])',
+ 'i18n:not([lang=ja])'
+ ]
+ }
+ ]
+ },
+ ,
+ {
+ code: '',
+ output: null,
+ options: [
+ {
+ order: [
+ 'template',
+ 'script:not([setup])',
+ 'script[setup]',
+ 'style[scoped]',
+ 'style:not([scoped])',
+ 'i18n[lang=en]',
+ 'i18n[lang=ja]'
+ ]
+ }
+ ]
+ },
{
code: '',
output: null,
options: [{ order: [['docs', 'script', 'template'], 'style'] }]
},
+ {
+ code: '',
+ output: null,
+ options: [{ order: ['i18n[locale=en]', 'i18n[locale=ja]'] }]
+ },
+ {
+ code: '',
+ output: null,
+ options: [{ order: ['style:not([scoped])', 'style[scoped]'] }]
+ },
``,
@@ -119,12 +200,12 @@ tester.run('component-tags-order', rule, {
code: '',
errors: [
{
- message: 'The should be above the \n '
+ },
+ {
+ code: '',
+ output: '',
+ options: [{ order: ['i18n[locale=en]', 'i18n[locale=ja]'] }],
+ errors: [
+ {
+ message:
+ ' should be above on line 1.',
+ line: 1
+ }
+ ]
+ },
+ {
+ code: '',
+ output: '',
+ options: [{ order: ['style:not([scoped])', 'style[scoped]'] }],
+ errors: [
+ {
+ message: '',
+ output: '',
+ options: [{ order: ['style[scoped]', 'style:not([scoped])'] }],
+ errors: [
+ {
+ message: '',
+ output: '',
+ options: [{ order: ['script:not([scoped])', 'style:not([scoped])'] }],
+ errors: [
+ {
+ message: '