Repository: vuejs/eslint-plugin-vue (js)

Issue:
vue/component-tags-order - Could it be made fixable?
**What rule do you want to change?**

`vue/component-tags-order`

**Does this change cause the rule to produce more or fewer warnings?**

No

**How will the change be implemented? (New option, new default behavior, etc.)?**

I wish this rule could be auto-changed on `--fix`

**Please provide some example code that this change will affect:**

None. Rule stays the same.

**What does the rule currently do for this code?**

No changes.

**What will the rule do after it's changed?**

On --fix, it will swap a single-file component's main tags around so they're in the order prescribed by the rule.

**Additional context**

I've been thinking of putting together a regex that would allow me to run a mass replacement on my project to ensure all tags are in the proper order, and it seems like it's probably possible. If a regex can do it, it makes me think that it's probably possible too, and easier, for a tool that "understands" the code like ESLint.

Pull request description:
Auto-fix for vue/component-tags-order
Fixes #1463.
This PR reflects the comments from @ota-meshi in #1512 . It yields a fix for each reported error, so eslint-disable comments work normally. Added a test case for eslint-disable comment.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 5b2d2229..3d4a9b2e 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -167,7 +167,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | Rule ID | Description |    |
 |:--------|:------------|:---|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
-| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements |  |
+| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | :wrench: |
 | [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` |  |
 | [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  |
@@ -283,7 +283,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | Rule ID | Description |    |
 |:--------|:------------|:---|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
-| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements |  |
+| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | :wrench: |
 | [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` |  |
 | [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  |
diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md
index bd47f2d8..9b053ab2 100644
--- a/docs/rules/component-tags-order.md
+++ b/docs/rules/component-tags-order.md
@@ -10,6 +10,7 @@ since: v6.1.0
 > enforce order of component top-level elements
 
 - :gear: This rule is included in `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
 
 ## :book: Rule Details
 
@@ -29,7 +30,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
 
 ### `{ "order": [ [ "script", "template" ], "style" ] }` (default)
 
-<eslint-code-block :rules="{'vue/component-tags-order': ['error']}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error']}">
 
 ```vue
 <!-- ✓ GOOD -->
@@ -40,7 +41,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
 
 </eslint-code-block>
 
-<eslint-code-block :rules="{'vue/component-tags-order': ['error']}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error']}">
 
 ```vue
 <!-- ✓ GOOD -->
@@ -51,7 +52,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
 
 </eslint-code-block>
 
-<eslint-code-block :rules="{'vue/component-tags-order': ['error']}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error']}">
 
 ```vue
 <!-- ✗ BAD -->
@@ -64,7 +65,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
 
 ### `{ "order": ["template", "script", "style"] }`
 
-<eslint-code-block :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script', 'style'] }]}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script', 'style'] }]}">
 
 ```vue
 <!-- ✓ GOOD -->
@@ -75,7 +76,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
 
 </eslint-code-block>
 
-<eslint-code-block :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script', 'style'] }]}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script', 'style'] }]}">
 
 ```vue
 <!-- ✗ BAD -->
@@ -88,7 +89,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
 
 ### `{ "order": ["docs", "template", "script", "style"] }`
 
-<eslint-code-block :rules="{'vue/component-tags-order': ['error', { 'order': ['docs', 'template', 'script', 'style'] }]}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['docs', 'template', 'script', 'style'] }]}">
 
 ```vue
 <!-- ✓ GOOD -->
@@ -100,7 +101,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
 
 </eslint-code-block>
 
-<eslint-code-block :rules="{'vue/component-tags-order': ['error', { 'order': ['docs', 'template', 'script', 'style'] }]}">
+<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['docs', 'template', 'script', 'style'] }]}">
 
 ```vue
 <!-- ✗ BAD -->
diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js
index dc7da972..afb624f9 100644
--- a/lib/rules/component-tags-order.js
+++ b/lib/rules/component-tags-order.js
@@ -24,7 +24,7 @@ module.exports = {
       categories: ['vue3-recommended', 'recommended'],
       url: 'https://eslint.vuejs.org/rules/component-tags-order.html'
     },
-    fixable: null,
+    fixable: 'code',
     schema: [
       {
         type: 'object',
@@ -87,30 +87,13 @@ module.exports = {
       return []
     }
 
-    /**
-     * @param {VElement} element
-     * @param {VElement} firstUnorderedElement
-     */
-    function report(element, firstUnorderedElement) {
-      context.report({
-        node: element,
-        loc: element.loc,
-        messageId: 'unexpected',
-        data: {
-          name: element.name,
-          firstUnorderedName: firstUnorderedElement.name,
-          line: firstUnorderedElement.loc.start.line
-        }
-      })
-    }
-
     return {
       Program(node) {
         if (utils.hasInvalidEOF(node)) {
           return
         }
         const elements = getTopLevelHTMLElements()
-
+        const sourceCode = context.getSourceCode()
         elements.forEach((element, index) => {
           const expectedIndex = getOrderPosition(element.name)
           if (expectedIndex < 0) {
@@ -123,7 +106,35 @@ module.exports = {
               (e1, e2) => getOrderPosition(e1.name) - getOrderPosition(e2.name)
             )[0]
           if (firstUnordered) {
-            report(element, firstUnordered)
+            context.report({
+              node: element,
+              loc: element.loc,
+              messageId: 'unexpected',
+              data: {
+                name: element.name,
+                firstUnorderedName: firstUnordered.name,
+                line: firstUnordered.loc.start.line
+              },
+              *fix(fixer) {
+                // insert element before firstUnordered
+                const fixedElements = elements.flatMap((it) => {
+                  if (it === firstUnordered) {
+                    return [element, it]
+                  } else if (it === element) {
+                    return []
+                  }
+                  return [it]
+                })
+                for (let i = elements.length - 1; i >= 0; i--) {
+                  if (elements[i] !== fixedElements[i]) {
+                    yield fixer.replaceTextRange(
+                      elements[i].range,
+                      sourceCode.text.slice(...fixedElements[i].range)
+                    )
+                  }
+                }
+              }
+            })
           }
         })
       }


Test patch:
diff --git a/tests/lib/rules/component-tags-order.js b/tests/lib/rules/component-tags-order.js
index 0c40d499..1992d6fd 100644
--- a/tests/lib/rules/component-tags-order.js
+++ b/tests/lib/rules/component-tags-order.js
@@ -9,6 +9,26 @@
 
 const rule = require('../../../lib/rules/component-tags-order')
 const RuleTester = require('eslint').RuleTester
+const assert = require('assert')
+const { ESLint } = require('../../eslint-compat')
+
+// Initialize linter.
+const eslint = new ESLint({
+  overrideConfig: {
+    parser: require.resolve('vue-eslint-parser'),
+    parserOptions: {
+      ecmaVersion: 2015
+    },
+    plugins: ['vue'],
+    rules: {
+      'vue/comment-directive': 'error',
+      'vue/component-tags-order': 'error'
+    }
+  },
+  useEslintrc: false,
+  plugins: { vue: require('../../../lib/index') },
+  fix: true
+})
 
 // ------------------------------------------------------------------------------
 // Tests
@@ -108,7 +128,8 @@ tester.run('component-tags-order', rule, {
           line: 1,
           column: 37
         }
-      ]
+      ],
+      output: '<template></template><style></style><script></script>'
     },
     {
       code: '<template></template><script></script><style></style>',
@@ -119,7 +140,8 @@ tester.run('component-tags-order', rule, {
           line: 1,
           column: 22
         }
-      ]
+      ],
+      output: '<script></script><template></template><style></style>'
     },
     {
       code: `
@@ -133,7 +155,14 @@ tester.run('component-tags-order', rule, {
           message: 'The <script> should be above the <style> on line 4.',
           line: 6
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '\n' +
+        '        <script></script>\n' +
+        '\n' +
+        '        <style></style>'
     },
     {
       code: `
@@ -147,7 +176,13 @@ tester.run('component-tags-order', rule, {
           message: 'The <script> should be above the <template> on line 2.',
           line: 3
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <script></script>\n' +
+        '        <template></template>\n' +
+        '        <style></style>\n' +
+        '      '
     },
     {
       code: `
@@ -161,7 +196,13 @@ tester.run('component-tags-order', rule, {
           message: 'The <template> should be above the <script> on line 2.',
           line: 3
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '        <script></script>\n' +
+        '        <style></style>\n' +
+        '      '
     },
     {
       code: `
@@ -176,7 +217,14 @@ tester.run('component-tags-order', rule, {
           message: 'The <docs> should be above the <template> on line 2.',
           line: 3
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <docs></docs>\n' +
+        '        <template></template>\n' +
+        '        <script></script>\n' +
+        '        <style></style>\n' +
+        '      '
     },
     {
       code: `
@@ -191,7 +239,14 @@ tester.run('component-tags-order', rule, {
           message: 'The <script> should be above the <template> on line 2.',
           line: 4
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <script></script>\n' +
+        '        <template></template>\n' +
+        '        <docs></docs>\n' +
+        '        <style></style>\n' +
+        '      '
     },
     {
       code: `
@@ -207,7 +262,15 @@ tester.run('component-tags-order', rule, {
           message: 'The <script> should be above the <template> on line 2.',
           line: 5
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <script></script>\n' +
+        '        <template></template>\n' +
+        '        <docs>\n' +
+        '        </docs>\n' +
+        '        <style></style>\n' +
+        '      '
     },
     {
       code: `
@@ -220,7 +283,9 @@ tester.run('component-tags-order', rule, {
           message: 'The <template> should be above the <script> on line 2.',
           line: 3
         }
-      ]
+      ],
+      output:
+        '\n        <template></template>\n        <script></script>\n      '
     },
     {
       code: `
@@ -237,7 +302,13 @@ tester.run('component-tags-order', rule, {
           message: 'The <script> should be above the <style> on line 2.',
           line: 4
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '        <style></style>\n' +
+        '        <script></script>\n' +
+        '      '
     },
     {
       code: `
@@ -255,7 +326,14 @@ tester.run('component-tags-order', rule, {
           message: 'The <script> should be above the <style> on line 2.',
           line: 5
         }
-      ]
+      ],
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '        <style></style>\n' +
+        '        <docs></docs>\n' +
+        '        <script></script>\n' +
+        '      '
     },
     // no <template>
     {
@@ -268,7 +346,27 @@ tester.run('component-tags-order', rule, {
           message: 'The <script> should be above the <style> on line 2.',
           line: 3
         }
-      ]
+      ],
+      output: '\n        <script></script>\n        <style></style>\n      '
     }
   ]
 })
+
+describe('suppress reporting with eslint-disable-next-line', () => {
+  it('do not report if <!-- eslint-disable-next-line -->', async () => {
+    const code = `<style></style><template></template>
+    <!-- eslint-disable-next-line vue/component-tags-order -->
+    <script></script>`
+    const [{ messages, output }] = await eslint.lintText(code, {
+      filePath: 'test.vue'
+    })
+    assert.strictEqual(messages.length, 0)
+    // should not fix <script>
+    assert.strictEqual(
+      output,
+      `<template></template><style></style>
+    <!-- eslint-disable-next-line vue/component-tags-order -->
+    <script></script>`
+    )
+  })
+})