From 366977e81f29eb3382de0d07ee514fe01b3da3b8 Mon Sep 17 00:00:00 2001 From: Douglas Wade Date: Wed, 23 Mar 2022 18:59:01 -0700 Subject: [PATCH 01/16] Fix #1819: Enforce order between script and script setup --- docs/rules/component-tags-order.md | 25 ++++++++++++++++--------- lib/rules/component-tags-order.js | 16 ++++++++++++++-- tests/lib/rules/component-tags-order.js | 12 ++++++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md index 9b053ab21..f0f208722 100644 --- a/docs/rules/component-tags-order.md +++ b/docs/rules/component-tags-order.md @@ -14,27 +14,28 @@ since: v6.1.0 ## :book: Rule Details -This rule warns about the order of the ` + ``` @@ -47,6 +48,7 @@ This rule warns about the order of the ` + ``` @@ -57,6 +59,7 @@ This rule warns about the order of the ` ``` @@ -65,21 +68,23 @@ This rule warns about the order of the ` ``` - + ```vue + @@ -87,26 +92,28 @@ This rule warns about the order of the ` + ``` - + ```vue + documentation ``` diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js index afb624f9f..d8e2e01c9 100644 --- a/lib/rules/component-tags-order.js +++ b/lib/rules/component-tags-order.js @@ -86,6 +86,12 @@ module.exports = { } return [] } + /** + * @param {VElement} element + */ + function isSetupScript(element) { + return element.name === 'script' && utils.getAttribute(element, 'setup') + } return { Program(node) { @@ -101,6 +107,12 @@ module.exports = { } const firstUnordered = elements .slice(0, index) + .map((e) => { + return { + element: e, + name: isSetupScript(e) ? 'script/setup' : e.name + } + }) .filter((e) => expectedIndex < getOrderPosition(e.name)) .sort( (e1, e2) => getOrderPosition(e1.name) - getOrderPosition(e2.name) @@ -113,12 +125,12 @@ module.exports = { data: { name: element.name, firstUnorderedName: firstUnordered.name, - 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 [] diff --git a/tests/lib/rules/component-tags-order.js b/tests/lib/rules/component-tags-order.js index 1992d6fd2..0f967e2fb 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,6 +104,16 @@ tester.run('component-tags-order', rule, { output: null, options: [{ order: ['docs', 'script', 'template', 'style'] }] }, + { + code: '', + output: null, + options: [{ order: ['script/setup', 'script', 'template', 'style'] }] + }, + { + code: '', + output: null, + options: [{ order: [['script/setup', 'script', 'template'], 'style'] }] + }, { code: '', output: null, From 7ea4ac4c61b97d22d3f12a5720f503d3863db254 Mon Sep 17 00:00:00 2001 From: Douglas Wade Date: Wed, 23 Mar 2022 19:16:34 -0700 Subject: [PATCH 02/16] update defaults to match documentation --- lib/rules/component-tags-order.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js index d8e2e01c9..4b3fa9e4f 100644 --- a/lib/rules/component-tags-order.js +++ b/lib/rules/component-tags-order.js @@ -10,7 +10,10 @@ const utils = require('../utils') -const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style']) +const DEFAULT_ORDER = Object.freeze([ + ['script', 'script/setup', 'template'], + 'style' +]) // ------------------------------------------------------------------------------ // Rule Definition From 9b9b218b842c94e0b002bb8e9b08fa3cc503e319 Mon Sep 17 00:00:00 2001 From: Douglas Wade Date: Sun, 27 Mar 2022 02:15:49 -0700 Subject: [PATCH 03/16] Use CSS selector syntax --- docs/rules/component-tags-order.md | 162 +++++++++++++++++++----- lib/rules/component-tags-order.js | 119 +++++++++++------ package.json | 1 + tests/lib/rules/component-tags-order.js | 37 +++++- 4 files changed, 249 insertions(+), 70 deletions(-) diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md index f0f208722..71fe5ad13 100644 --- a/docs/rules/component-tags-order.md +++ b/docs/rules/component-tags-order.md @@ -5,6 +5,7 @@ title: vue/component-tags-order description: enforce order of component top-level elements since: v6.1.0 --- + # vue/component-tags-order > enforce order of component top-level elements @@ -20,24 +21,33 @@ This rule warns about the order of the ` - + + - + ``` @@ -47,9 +57,15 @@ This rule warns about the order of the ` - - + + + ``` @@ -58,9 +74,15 @@ This rule warns about the order of the ` - + + + ``` @@ -68,54 +90,132 @@ This rule warns about the order of the ` - - + + + ``` - + ```vue - - + + - + ``` -### `{ "order": ["docs", "template", "script", "script/setup", "style"] }` +### `{ "order": ["docs", "template", "script", "script[setup]", "style"] }` - + ```vue documentation - - - + + + ``` - + ```vue - - + + documentation - + +``` + + + +### `{ "order": ["docs", "template", "script", "script[setup]", "style"] }` + + + +```vue + + +/* ... */ + + +/* ... */ + + + + + +``` + + + + + +```vue + + + + +/* ... */ + + +/* ... */ + + + ``` diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js index 4b3fa9e4f..fd2937c12 100644 --- a/lib/rules/component-tags-order.js +++ b/lib/rules/component-tags-order.js @@ -9,11 +9,9 @@ // ------------------------------------------------------------------------------ const utils = require('../utils') +const parser = require('postcss-selector-parser') -const DEFAULT_ORDER = Object.freeze([ - ['script', 'script/setup', 'template'], - 'style' -]) +const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style']) // ------------------------------------------------------------------------------ // Rule Definition @@ -46,11 +44,7 @@ module.exports = { }, additionalProperties: false } - ], - messages: { - unexpected: - 'The <{{name}}> should be above the <{{firstUnorderedName}}> on line {{line}}.' - } + ] }, /** * @param {RuleContext} context - The rule context. @@ -73,11 +67,74 @@ module.exports = { }) /** - * @param {string} name + * @param {VElement} element + * @return {String} + */ + 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 getOrderPosition(name) { - const num = orderMap.get(name) - return num == null ? -1 : num + function matches(ordering, element) { + let hasMatch = true + let isNegated = false + + parser((selectors) => { + selectors.walk((selector) => { + switch (selector.type) { + case 'tag': + hasMatch = hasMatch && selector.value === element.name + break + case 'pseudo': + isNegated = selector.value === ':not' + break + case 'attribute': + hasMatch = + hasMatch && + utils.hasAttribute( + element, + selector.qualifiedAttribute, + selector.value + ) + break + } + }) + }).processSync(ordering) + + if (isNegated) { + return !hasMatch + } else { + return hasMatch + } + } + + /** + * @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 && @@ -89,12 +146,6 @@ module.exports = { } return [] } - /** - * @param {VElement} element - */ - function isSetupScript(element) { - return element.name === 'script' && utils.getAttribute(element, 'setup') - } return { Program(node) { @@ -104,36 +155,30 @@ 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) - .map((e) => { - return { - element: e, - name: isSetupScript(e) ? 'script/setup' : e.name - } - }) - .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, - firstUnorderedName: firstUnordered.name, - line: firstUnordered.element.loc.start.line - }, + message: `The <${element.name}${ + elementAttributes ? ' ' + elementAttributes : '' + }> should be above the <${firstUnordered.name}${ + firstUnorderedttributes ? ' ' + firstUnorderedttributes : '' + }> on line ${firstUnordered.loc.start.line}.`, *fix(fixer) { // insert element before firstUnordered const fixedElements = elements.flatMap((it) => { - if (it === firstUnordered.element) { + if (it === firstUnordered) { return [element, it] } else if (it === element) { return [] 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 0f967e2fb..3f1c6078c 100644 --- a/tests/lib/rules/component-tags-order.js +++ b/tests/lib/rules/component-tags-order.js @@ -107,18 +107,28 @@ tester.run('component-tags-order', rule, { { code: '', output: null, - options: [{ order: ['script/setup', 'script', 'template', 'style'] }] + options: [{ order: ['script[setup]', 'script', 'template', 'style'] }] }, { code: '', output: null, - options: [{ order: [['script/setup', 'script', 'template'], 'style'] }] + options: [{ order: [['script[setup]', 'script', 'template'], 'style'] }] }, { 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]'] }] + }, ``, @@ -360,6 +370,29 @@ tester.run('component-tags-order', rule, { } ], output: '\n \n \n ' + }, + { + code: '', + output: '', + options: [{ order: ['i18n[locale=en]', 'i18n[locale=ja]'] }], + errors: [ + { + message: + 'The should be above the on line 1.', + line: 1 + } + ] + }, + { + code: '', + output: '', + options: [{ order: ['style:not([scoped])', 'style[scoped]'] }], + errors: [ + { + message: 'The @@ -77,9 +70,6 @@ This rule warns about the order of the ` From 918fca1e35534f79e04493a312e01ec3bd555249 Mon Sep 17 00:00:00 2001 From: Douglas Wade Date: Fri, 1 Apr 2022 22:52:05 -0700 Subject: [PATCH 05/16] Fix bug with multiple not pseudo-selectors --- lib/rules/component-tags-order.js | 42 ++++++++----- tests/lib/rules/component-tags-order.js | 81 +++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js index fd2937c12..b9508acc9 100644 --- a/lib/rules/component-tags-order.js +++ b/lib/rules/component-tags-order.js @@ -26,6 +26,10 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/component-tags-order.html' }, fixable: 'code', + messages: { + unexpected: + 'The <{{elementName}}{{elementAttributes}}> should be above the <{{firstUnorderedName}}{{firstUnorderedAttributes}}> on line {{line}}.' + }, schema: [ { type: 'object', @@ -92,35 +96,34 @@ module.exports = { * @return {Boolean} true if the element matches the selector, false otherwise */ function matches(ordering, element) { - let hasMatch = true + let attributeMatches = true let isNegated = false + let tagMatches = true parser((selectors) => { selectors.walk((selector) => { switch (selector.type) { case 'tag': - hasMatch = hasMatch && selector.value === element.name + tagMatches = selector.value === element.name break case 'pseudo': isNegated = selector.value === ':not' break case 'attribute': - hasMatch = - hasMatch && - utils.hasAttribute( - element, - selector.qualifiedAttribute, - selector.value - ) + attributeMatches = utils.hasAttribute( + element, + selector.qualifiedAttribute, + selector.value + ) break } }) }).processSync(ordering) if (isNegated) { - return !hasMatch + return tagMatches && !attributeMatches } else { - return hasMatch + return tagMatches && attributeMatches } } @@ -170,11 +173,18 @@ module.exports = { context.report({ node: element, loc: element.loc, - message: `The <${element.name}${ - elementAttributes ? ' ' + elementAttributes : '' - }> should be above the <${firstUnordered.name}${ - firstUnorderedttributes ? ' ' + firstUnorderedttributes : '' - }> on line ${firstUnordered.loc.start.line}.`, + messageId: 'unexpected', + data: { + elementName: element.name, + elementAttributes: elementAttributes + ? ' ' + elementAttributes + : '', + firstUnorderedName: firstUnordered.name, + firstUnorderedAttributes: firstUnorderedttributes + ? ' ' + firstUnorderedttributes + : '', + line: firstUnordered.loc.start.line + }, *fix(fixer) { // insert element before firstUnordered const fixedElements = elements.flatMap((it) => { diff --git a/tests/lib/rules/component-tags-order.js b/tests/lib/rules/component-tags-order.js index 3f1c6078c..19d23b471 100644 --- a/tests/lib/rules/component-tags-order.js +++ b/tests/lib/rules/component-tags-order.js @@ -114,6 +114,65 @@ tester.run('component-tags-order', rule, { output: null, options: [{ order: [['script[setup]', 'script', 'template'], 'style'] }] }, + { + code: '', + output: null, + options: [{ order: ['script', 'script[setup]', 'template', 'style'] }] + }, + { + code: '', + output: null, + options: [{ order: [['script', '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'] + } + ] + }, + { + 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, @@ -393,6 +452,28 @@ tester.run('component-tags-order', rule, { line: 1 } ] + }, + { + code: '', + output: '', + options: [{ order: ['style[scoped]', 'style:not([scoped])'] }], + errors: [ + { + message: 'The ', + output: '', + options: [{ order: ['script:not([scoped])', 'style:not([scoped])'] }], + errors: [ + { + message: 'The + - + ``` @@ -53,12 +46,8 @@ This rule warns about the order of the ` - + + ``` @@ -67,12 +56,8 @@ This rule warns about the order of the ` + + ``` @@ -80,132 +65,50 @@ This rule warns about the order of the ` - - + + ``` - + ```vue - - + - + ``` -### `{ "order": ["docs", "template", "script", "script[setup]", "style"] }` +### `{ "order": ["docs", "template", "script", "style"] }` - + ```vue documentation - - - + + ``` - + ```vue - - + documentation - -``` - - - -### `{ "order": ["docs", "template", "script", "script[setup]", "style"] }` - - - -```vue - - -/* ... */ - - -/* ... */ - - - - - -``` - - - - - -```vue - - - - -/* ... */ - - -/* ... */ - - - + ``` From e24dd4056afb586bed4050903a0753d683277452 Mon Sep 17 00:00:00 2001 From: Douglas Wade Date: Sat, 2 Apr 2022 23:20:54 -0700 Subject: [PATCH 07/16] update docs --- docs/rules/component-tags-order.md | 37 ++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md index 9b053ab21..7855c483c 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 + + + + +``` + + + + + +```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) From 7e0e9656338546996de839958793fa71dd833a15 Mon Sep 17 00:00:00 2001 From: Douglas Wade Date: Sat, 2 Apr 2022 23:25:12 -0700 Subject: [PATCH 08/16] Update error message --- lib/rules/component-tags-order.js | 2 +- tests/lib/rules/component-tags-order.js | 38 ++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js index b9508acc9..02ddfef5d 100644 --- a/lib/rules/component-tags-order.js +++ b/lib/rules/component-tags-order.js @@ -28,7 +28,7 @@ module.exports = { fixable: 'code', messages: { unexpected: - 'The <{{elementName}}{{elementAttributes}}> should be above the <{{firstUnorderedName}}{{firstUnorderedAttributes}}> on line {{line}}.' + '<{{elementName}}{{elementAttributes}}> should be above <{{firstUnorderedName}}{{firstUnorderedAttributes}}> on line {{line}}.' }, schema: [ { diff --git a/tests/lib/rules/component-tags-order.js b/tests/lib/rules/component-tags-order.js index 19d23b471..be312edb7 100644 --- a/tests/lib/rules/component-tags-order.js +++ b/tests/lib/rules/component-tags-order.js @@ -200,12 +200,12 @@ tester.run('component-tags-order', rule, { code: '', errors: [ { - message: 'The