Skip to content

Add autofix to vue/prefer-use-template-ref #2632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(prefer-use-template-ref): add support for fix option
  • Loading branch information
Thomasan1999 committed Nov 30, 2024
commit ac5aa922cdc358acb9c8664d94cd91965f89efce
2 changes: 1 addition & 1 deletion docs/rules/index.md
Original file line number Diff line number Diff line change
@@ -270,7 +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`/`shallowRef` for template refs | | :hammer: |
| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs | :wrench: | :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: |
6 changes: 4 additions & 2 deletions docs/rules/prefer-use-template-ref.md
Original file line number Diff line number Diff line change
@@ -10,14 +10,16 @@ since: v9.31.0

> require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs

- :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

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`/`shallowRef` for template refs.

<eslint-code-block :rules="{'vue/prefer-use-template-ref': ['error']}">
<eslint-code-block fix :rules="{'vue/prefer-use-template-ref': ['error']}">

```vue
<template>
@@ -45,7 +47,7 @@ This rule enforces using the new `useTemplateRef` function instead of `ref`/`sha
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']}">
<eslint-code-block fix :rules="{'vue/prefer-use-template-ref': ['error']}">

```vue
<template>
82 changes: 82 additions & 0 deletions lib/rules/prefer-use-template-ref.js
Original file line number Diff line number Diff line change
@@ -44,6 +44,59 @@ function getScriptRefsFromSetupFunction(body) {
return refDeclarators.map(convertDeclaratorToScriptRef)
}

/** @param node {Statement | ModuleDeclaration} */
function createIndent(node) {
const indentSize = node.loc.start.column

return ' '.repeat(indentSize)
}

/**
* @param context {RuleContext}
* @param fixer {RuleFixer}
* */
function addUseTemplateRefImport(context, fixer) {
const sourceCode = context.sourceCode

if (!sourceCode) {
return
}

const body = sourceCode.ast.body

const imports = body.filter((node) => node.type === 'ImportDeclaration')

const vueDestructuredImport = imports.find(
(importStatement) =>
importStatement.source.value === 'vue' &&
importStatement.specifiers.some(
(specifier) => specifier.type === 'ImportSpecifier'
)
)

if (vueDestructuredImport) {
const importSpecifierLast = vueDestructuredImport.specifiers.at(-1)

// @ts-ignore
return fixer.insertTextAfter(importSpecifierLast, `,useTemplateRef`)
}

const lastImport = imports.at(-1)

const importStatement = "import {useTemplateRef} from 'vue';"

if (lastImport) {
const indent = createIndent(lastImport)

return fixer.insertTextAfter(lastImport, `\n${indent}${importStatement}`)
}

const firstNode = body[0]
const indent = createIndent(firstNode)

return fixer.insertTextBefore(firstNode, `${importStatement}\n${indent}`)
}

/** @type {import("eslint").Rule.RuleModule} */
module.exports = {
meta: {
@@ -54,6 +107,7 @@ module.exports = {
categories: undefined,
url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html'
},
fixable: 'code',
schema: [],
messages: {
preferUseTemplateRef: "Replace '{{name}}' with 'useTemplateRef'."
@@ -93,6 +147,8 @@ module.exports = {
}),
{
'Program:exit'() {
let missingImportChecked = false

for (const templateRef of templateRefs) {
const scriptRef = scriptRefs.find(
(scriptRef) => scriptRef.ref === templateRef
@@ -108,6 +164,32 @@ module.exports = {
data: {
// @ts-ignore
name: scriptRef.node?.callee?.name
},
fix(fixer) {
/** @type {Fix[]} */
const fixes = []

const replaceFunctionFix = fixer.replaceText(
scriptRef.node,
`useTemplateRef('${scriptRef.ref}')`
)

fixes.push(replaceFunctionFix)

if (!missingImportChecked) {
missingImportChecked = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this flag for?
Perhaps to avoid autofix conflicts?
If so, ESLint's autofixes avoid conflicts as much as possible by core, so there's no need to handle that in the rules.


const missingImportFix = addUseTemplateRefImport(
context,
fixer
)

if (missingImportFix) {
fixes.push(missingImportFix)
}
}

return fixes
}
})
}
152 changes: 152 additions & 0 deletions tests/lib/rules/prefer-use-template-ref.js
Original file line number Diff line number Diff line change
@@ -266,6 +266,15 @@ tester.run('prefer-use-template-ref', rule, {
const root = ref();
</script>
`,
output: `
<template>
<div ref="root"/>
</template>
<script setup>
import { ref,useTemplateRef } from 'vue';
const root = useTemplateRef('root');
</script>
`,
errors: [
{
messageId: 'preferUseTemplateRef',
@@ -290,6 +299,17 @@ tester.run('prefer-use-template-ref', rule, {
const link = ref();
</script>
`,
output: `
<template>
<button ref="button">Content</button>
<a href="" ref="link">Link</a>
</template>
<script setup>
import { ref,useTemplateRef } from 'vue';
const buttonRef = ref();
const link = useTemplateRef('link');
</script>
`,
errors: [
{
messageId: 'preferUseTemplateRef',
@@ -314,6 +334,17 @@ tester.run('prefer-use-template-ref', rule, {
const link = ref();
</script>
`,
output: `
<template>
<h1 ref="heading">Heading</h1>
<a href="" ref="link">Link</a>
</template>
<script setup>
import { ref,useTemplateRef } from 'vue';
const heading = useTemplateRef('heading');
const link = useTemplateRef('link');
</script>
`,
errors: [
{
messageId: 'preferUseTemplateRef',
@@ -351,6 +382,22 @@ tester.run('prefer-use-template-ref', rule, {
}
</script>
`,
output: `
<template>
<p>Button clicked {{counter}} times.</p>
<button ref="button">Click</button>
</template>
<script>
import { ref,useTemplateRef } from 'vue';
export default {
name: 'Counter',
setup() {
const counter = ref(0);
const button = useTemplateRef('button');
}
}
</script>
`,
errors: [
{
messageId: 'preferUseTemplateRef',
@@ -373,6 +420,15 @@ tester.run('prefer-use-template-ref', rule, {
const root = shallowRef();
</script>
`,
output: `
<template>
<div ref="root"/>
</template>
<script setup>
import { shallowRef,useTemplateRef } from 'vue';
const root = useTemplateRef('root');
</script>
`,
errors: [
{
messageId: 'preferUseTemplateRef',
@@ -383,6 +439,102 @@ tester.run('prefer-use-template-ref', rule, {
column: 22
}
]
},
{
filename: 'missing-import.vue',
code: `
<template>
<p>Button clicked {{counter}} times.</p>
<button ref="button">Click</button>
</template>
<script>
import { isEqual } from 'lodash';
export default {
name: 'Counter',
setup() {
const counter = ref(0);
if (isEqual(counter.value, 0)) {
console.log('Counter is reset');
}
const button = ref();
}
}
</script>
`,
output: `
<template>
<p>Button clicked {{counter}} times.</p>
<button ref="button">Click</button>
</template>
<script>
import { isEqual } from 'lodash';
import {useTemplateRef} from 'vue';
export default {
name: 'Counter',
setup() {
const counter = ref(0);
if (isEqual(counter.value, 0)) {
console.log('Counter is reset');
}
const button = useTemplateRef('button');
}
}
</script>
`,
errors: [
{
messageId: 'preferUseTemplateRef',
data: {
name: 'ref'
},
line: 15,
column: 28
}
]
},
{
filename: 'no-imports.vue',
code: `
<template>
<p>Button clicked {{counter}} times.</p>
<button ref="button">Click</button>
</template>
<script>
export default {
name: 'Counter',
setup() {
const counter = ref(0);
const button = ref();
}
}
</script>
`,
output: `
<template>
<p>Button clicked {{counter}} times.</p>
<button ref="button">Click</button>
</template>
<script>
import {useTemplateRef} from 'vue';
export default {
name: 'Counter',
setup() {
const counter = ref(0);
const button = useTemplateRef('button');
}
}
</script>
`,
errors: [
{
messageId: 'preferUseTemplateRef',
data: {
name: 'ref'
},
line: 11,
column: 28
}
]
}
]
})