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