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

Issue:
autofix for vue/no-empty-component-block
<!--
  Before proposing rule changes, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**What rule do you want to change?**
- Rule to change is vue/no-empty-component-block

**Does this change cause the rule to produce more or fewer warnings?**
- This change would not change the amount of errors/warnings

**How will the change be implemented? (New option, new default behavior, etc.)?**
- Preferably default behaviour

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```vue
<script setup lang="ts">
  defineProps<{...}>()
</script>

<template>
  <h1>My awesome title</h1>
</template>

<style scoped lang="postcss"></style>
```

**What does the rule currently do for this code?**
- Currently the rule marks the style tag as an error, but you would have to remove it manually

**What will the rule do after it's changed?**
- After change eslint will be able to remove the style tag with `pnpm eslint --fix`

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->



Pull request:
feat(no-empty-component-block): support auto fix
resolve #2564

~~Chose this approach is because I couldn't find a better way to handle reporting & fixing each `emptyBlock` within a for loop,  as removing the first block will affect the range of the second block😵‍💫.~~

UPDATE: support auto delete empty blocks



Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 1ba5978d..e359f47a 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -232,7 +232,7 @@ For example:
 | [vue/no-deprecated-delete-set](./no-deprecated-delete-set.md) | disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+) |  | :warning: |
 | [vue/no-deprecated-model-definition](./no-deprecated-model-definition.md) | disallow deprecated `model` definition (in Vue.js 3.0.0+) | :bulb: | :warning: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  | :hammer: |
-| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  | :hammer: |
+| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty | :wrench: | :hammer: |
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  | :hammer: |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
 | [vue/no-ref-object-reactivity-loss](./no-ref-object-reactivity-loss.md) | disallow usages of ref objects that can lead to loss of reactivity |  | :warning: |
@@ -270,6 +270,7 @@ For example:
 | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
 | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
 | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
+| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref` for template refs |  | :hammer: |
 | [vue/require-default-export](./require-default-export.md) | require components to be the default export |  | :warning: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  | :hammer: |
 | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |
diff --git a/docs/rules/no-empty-component-block.md b/docs/rules/no-empty-component-block.md
index 36ea4c07..d0e3dbd1 100644
--- a/docs/rules/no-empty-component-block.md
+++ b/docs/rules/no-empty-component-block.md
@@ -10,6 +10,8 @@ since: v7.0.0
 
 > disallow the `<template>` `<script>` `<style>` block to be empty
 
+- :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
 
 This rule disallows the `<template>` `<script>` `<style>` block to be empty.
@@ -17,7 +19,7 @@ This rule disallows the `<template>` `<script>` `<style>` block to be empty.
 This rule also checks block what has attribute `src`.
 See [Vue Single-File Component (SFC) Spec](https://vue-loader.vuejs.org/spec.html#src-imports).
 
-<eslint-code-block :rules="{'vue/no-empty-component-block': ['error']}">
+<eslint-code-block fix :rules="{'vue/no-empty-component-block': ['error']}">
 
 ```vue
 <!-- ✓ GOOD -->
diff --git a/docs/rules/prefer-use-template-ref.md b/docs/rules/prefer-use-template-ref.md
new file mode 100644
index 00000000..6d7fde89
--- /dev/null
+++ b/docs/rules/prefer-use-template-ref.md
@@ -0,0 +1,71 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/prefer-use-template-ref
+description: require using `useTemplateRef` instead of `ref` for template refs
+---
+
+# vue/prefer-use-template-ref
+
+> require using `useTemplateRef` instead of `ref` for template refs
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
+
+## :book: Rule Details
+
+Vue 3.5 introduced a new way of obtaining template refs via
+the [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API.
+
+This rule enforces using the new `useTemplateRef` function instead of `ref` for template refs.
+
+<eslint-code-block :rules="{'vue/prefer-use-template-ref': ['error']}">
+
+```vue
+<template>
+  <button ref="submitRef">Submit</button>
+  <button ref="closeRef">Close</button>
+</template>
+
+<script setup>
+  import { ref, useTemplateRef } from 'vue';
+
+  /* ✓ GOOD */
+  const submitRef = useTemplateRef('submitRef');
+  const submitButton = useTemplateRef('submitRef');
+  const closeButton = useTemplateRef('closeRef');
+
+  /* ✗ BAD */
+  const closeRef = ref();
+</script>
+```
+
+</eslint-code-block>
+
+This rule skips `ref` template function refs as these should be used to allow custom implementation of storing `ref`. If you prefer
+`useTemplateRef`, you have to change the value of the template `ref` to a string.
+
+<eslint-code-block :rules="{'vue/prefer-use-template-ref': ['error']}">
+
+```vue
+<template>
+  <button :ref="ref => button = ref">Content</button>
+</template>
+
+<script setup>
+  import { ref } from 'vue';
+  
+  /* ✓ GOOD */
+  const button = ref();
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-use-template-ref.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-use-template-ref.js)
diff --git a/lib/index.js b/lib/index.js
index bb4abf40..b09e247d 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -208,6 +208,7 @@ const plugin = {
     'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
     'prefer-template': require('./rules/prefer-template'),
     'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
+    'prefer-use-template-ref': require('./rules/prefer-use-template-ref'),
     'prop-name-casing': require('./rules/prop-name-casing'),
     'quote-props': require('./rules/quote-props'),
     'require-component-is': require('./rules/require-component-is'),
diff --git a/lib/rules/no-empty-component-block.js b/lib/rules/no-empty-component-block.js
index b2e428bc..e7afc59f 100644
--- a/lib/rules/no-empty-component-block.js
+++ b/lib/rules/no-empty-component-block.js
@@ -45,7 +45,7 @@ module.exports = {
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/no-empty-component-block.html'
     },
-    fixable: null,
+    fixable: 'code',
     schema: [],
     messages: {
       unexpected: '`<{{ blockName }}>` is empty. Empty block is not allowed.'
@@ -91,6 +91,9 @@ module.exports = {
               messageId: 'unexpected',
               data: {
                 blockName: componentBlock.name
+              },
+              fix(fixer) {
+                return fixer.remove(componentBlock)
               }
             })
           }
diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js
new file mode 100644
index 00000000..8dcdccb3
--- /dev/null
+++ b/lib/rules/prefer-use-template-ref.js
@@ -0,0 +1,89 @@
+/**
+ * @author Thomasan1999
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/** @param expression {Expression | null} */
+function expressionIsRef(expression) {
+  // @ts-ignore
+  return expression?.callee?.name === 'ref'
+}
+
+/** @type {import("eslint").Rule.RuleModule} */
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'require using `useTemplateRef` instead of `ref` for template refs',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html'
+    },
+    schema: [],
+    messages: {
+      preferUseTemplateRef: "Replace 'ref' with 'useTemplateRef'."
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type Set<string> */
+    const templateRefs = new Set()
+
+    /**
+     * @typedef ScriptRef
+     * @type {{node: Expression, ref: string}}
+     */
+
+    /**
+     * @type ScriptRef[] */
+    const scriptRefs = []
+
+    return utils.compositingVisitors(
+      utils.defineTemplateBodyVisitor(
+        context,
+        {
+          'VAttribute[directive=false]'(node) {
+            if (node.key.name === 'ref' && node.value?.value) {
+              templateRefs.add(node.value.value)
+            }
+          }
+        },
+        {
+          VariableDeclarator(declarator) {
+            if (!expressionIsRef(declarator.init)) {
+              return
+            }
+
+            scriptRefs.push({
+              // @ts-ignore
+              node: declarator.init,
+              // @ts-ignore
+              ref: declarator.id.name
+            })
+          }
+        }
+      ),
+      {
+        'Program:exit'() {
+          for (const templateRef of templateRefs) {
+            const scriptRef = scriptRefs.find(
+              (scriptRef) => scriptRef.ref === templateRef
+            )
+
+            if (!scriptRef) {
+              continue
+            }
+
+            context.report({
+              node: scriptRef.node,
+              messageId: 'preferUseTemplateRef'
+            })
+          }
+        }
+      }
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-empty-component-block.js b/tests/lib/rules/no-empty-component-block.js
index e54fd9df..0a014099 100644
--- a/tests/lib/rules/no-empty-component-block.js
+++ b/tests/lib/rules/no-empty-component-block.js
@@ -27,6 +27,7 @@ tester.run('no-empty-component-block', rule, {
   invalid: [
     {
       code: `<template></template>`,
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -35,6 +36,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: `<template> </template>`,
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -44,6 +46,7 @@ tester.run('no-empty-component-block', rule, {
     {
       code: `<template>
 </template>`,
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -52,6 +55,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template />',
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -60,6 +64,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template src="" />',
+      output: '',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -68,6 +73,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template></template><script></script>',
+      output: '<script></script>',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -79,6 +85,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template /><script />',
+      output: '<script />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -90,6 +97,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template src="" /><script src="" />',
+      output: '<script src="" />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -101,6 +109,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template></template><script></script><style></style>',
+      output: '<script></script>',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -115,6 +124,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template /><script /><style />',
+      output: '<script />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -129,6 +139,7 @@ tester.run('no-empty-component-block', rule, {
     },
     {
       code: '<template src="" /><script src="" /><style src="" />',
+      output: '<script src="" />',
       errors: [
         {
           message: '`<template>` is empty. Empty block is not allowed.'
@@ -140,6 +151,64 @@ tester.run('no-empty-component-block', rule, {
           message: '`<style>` is empty. Empty block is not allowed.'
         }
       ]
+    },
+    // auto fix with whitespace
+    {
+      code: '<template></template> <script></script> <style></style>',
+      output: '  ',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template /> <script /> <style />',
+      output: '  ',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template src="" /> <script src="" /> <style src="" />',
+      output: '  ',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template><p></p></template> <script src="" /> <style src="" />',
+      output: '<template><p></p></template>  ',
+      errors: [
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js
new file mode 100644
index 00000000..49a2f075
--- /dev/null
+++ b/tests/lib/rules/prefer-use-template-ref.js
@@ -0,0 +1,323 @@
+/**
+ * @author Thomasan1999
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/prefer-use-template-ref')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('prefer-use-template-ref', rule, {
+  valid: [
+    {
+      filename: 'single-use-template-ref.vue',
+      code: `
+      <template>
+          <div ref="root" />
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        const root = useTemplateRef('root');
+      </script>
+      `
+    },
+    {
+      filename: 'multiple-use-template-refs.vue',
+      code: `
+      <template>
+          <button ref="button">Content</button>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        const buttonRef = useTemplateRef('button');
+        const link = useTemplateRef('link');
+      </script>
+      `
+    },
+    {
+      filename: 'use-template-ref-in-block.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li ref="firstListItem">Morning</li>
+              <li>Afternoon</li>
+              <li>Evening</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        function getFirstListItemElement() {
+          const firstListItem = useTemplateRef('firstListItem')
+          console.log(firstListItem)
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'non-template-ref.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li v-for="food in foods" :key="food">{{food}}</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const foods = ref(['Spaghetti', 'Pizza', 'Cake']);
+      </script>
+      `
+    },
+    {
+      filename: 'counter.js',
+      code: `
+      import { ref } from 'vue';
+      const counter = ref(0);
+      const names = ref(new Set());
+      function incrementCounter() {
+        counter.value++;
+        return counter.value;
+      }
+      function storeName(name) {
+        names.value.add(name)
+      }
+      `
+    },
+    {
+      filename: 'setup-function.vue',
+      code: `
+      <template>
+        <p>Button clicked {{counter}} times.</p>
+        <button ref="button" @click="counter++">Click</button>
+      </template>
+      <script>
+        import { ref, useTemplateRef } from 'vue';
+        export default {
+          name: 'Counter',
+          setup() {
+            const counter = ref(0);
+            const button = useTemplateRef('button');
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'options-api-no-refs.vue',
+      code: `
+      <template>
+        <label ref="label">
+          Name:
+          <input v-model="name" />
+        </label>
+        <p ref="textRef">{{niceName}}</p>
+        <button
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'NameRow',
+          methods: {
+            someFunction() {
+              return {
+                label: ref(5),
+              }
+            }
+          }
+          data() {
+            return {
+              label: 'label',
+              name: ''
+            }
+          },
+          computed: {
+            niceName() {
+              return 'Nice ' + this.name;
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'options-api-mixed.vue',
+      code: `
+      <template>
+        <label ref="labelElem">
+          Name:
+          <input v-model="name" />
+        </label>
+        {{ loremIpsum }}
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'NameRow',
+          props: {
+            defaultLabel: {
+              type: String,
+            },
+          },
+          data() {
+            return {
+              label: ref(this.defaultLabel),
+              labelElem: ref(),
+              name: ''
+            }
+          },
+          computed: {
+            loremIpsum() {
+              return this.name + ' lorem ipsum'
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'template-ref-function.vue',
+      code: `
+      <template>
+        <button :ref="ref => button = ref">Content</button>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const button = ref();
+        </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'single-ref.vue',
+      code: `
+      <template>
+          <div ref="root"/>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const root = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 7,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'one-ref-unused-in-script.vue',
+      code: `
+      <template>
+          <button ref="button">Content</button>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const buttonRef = ref();
+        const link = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 9,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'multiple-refs.vue',
+      code: `
+      <template>
+          <h1 ref="heading">Heading</h1>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const heading = ref();
+        const link = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 8,
+          column: 25
+        },
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 9,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'ref-in-block.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li ref="firstListItem">Morning</li>
+              <li>Afternoon</li>
+              <li>Evening</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        function getFirstListItemElement() {
+          const firstListItem = ref();
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 14,
+          column: 33
+        }
+      ]
+    },
+    {
+      filename: 'setup-function-only-refs.vue',
+      code: `
+      <template>
+        <p>Button clicked {{counter}} times.</p>
+        <button ref="button">Click</button>
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'Counter',
+          setup() {
+            const counter = ref(0);
+            const button = ref();
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 12,
+          column: 28
+        }
+      ]
+    }
+  ]
+})