Skip to content

Commit ac5aa92

Browse files
committed
feat(prefer-use-template-ref): add support for fix option
1 parent dc06535 commit ac5aa92

File tree

4 files changed

+239
-3
lines changed

4 files changed

+239
-3
lines changed

docs/rules/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ For example:
270270
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
271271
| [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: |
272272
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
273-
| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs | | :hammer: |
273+
| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs | :wrench: | :hammer: |
274274
| [vue/require-default-export](./require-default-export.md) | require components to be the default export | | :warning: |
275275
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | :hammer: |
276276
| [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |

docs/rules/prefer-use-template-ref.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ since: v9.31.0
1010

1111
> require using `useTemplateRef` instead of `ref`/`shallowRef` for template refs
1212
13+
- :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.
14+
1315
## :book: Rule Details
1416

1517
Vue 3.5 introduced a new way of obtaining template refs via
1618
the [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API.
1719

1820
This rule enforces using the new `useTemplateRef` function instead of `ref`/`shallowRef` for template refs.
1921

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

2224
```vue
2325
<template>
@@ -45,7 +47,7 @@ This rule enforces using the new `useTemplateRef` function instead of `ref`/`sha
4547
This rule skips `ref` template function refs as these should be used to allow custom implementation of storing `ref`. If you prefer
4648
`useTemplateRef`, you have to change the value of the template `ref` to a string.
4749

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

5052
```vue
5153
<template>

lib/rules/prefer-use-template-ref.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,59 @@ function getScriptRefsFromSetupFunction(body) {
4444
return refDeclarators.map(convertDeclaratorToScriptRef)
4545
}
4646

47+
/** @param node {Statement | ModuleDeclaration} */
48+
function createIndent(node) {
49+
const indentSize = node.loc.start.column
50+
51+
return ' '.repeat(indentSize)
52+
}
53+
54+
/**
55+
* @param context {RuleContext}
56+
* @param fixer {RuleFixer}
57+
* */
58+
function addUseTemplateRefImport(context, fixer) {
59+
const sourceCode = context.sourceCode
60+
61+
if (!sourceCode) {
62+
return
63+
}
64+
65+
const body = sourceCode.ast.body
66+
67+
const imports = body.filter((node) => node.type === 'ImportDeclaration')
68+
69+
const vueDestructuredImport = imports.find(
70+
(importStatement) =>
71+
importStatement.source.value === 'vue' &&
72+
importStatement.specifiers.some(
73+
(specifier) => specifier.type === 'ImportSpecifier'
74+
)
75+
)
76+
77+
if (vueDestructuredImport) {
78+
const importSpecifierLast = vueDestructuredImport.specifiers.at(-1)
79+
80+
// @ts-ignore
81+
return fixer.insertTextAfter(importSpecifierLast, `,useTemplateRef`)
82+
}
83+
84+
const lastImport = imports.at(-1)
85+
86+
const importStatement = "import {useTemplateRef} from 'vue';"
87+
88+
if (lastImport) {
89+
const indent = createIndent(lastImport)
90+
91+
return fixer.insertTextAfter(lastImport, `\n${indent}${importStatement}`)
92+
}
93+
94+
const firstNode = body[0]
95+
const indent = createIndent(firstNode)
96+
97+
return fixer.insertTextBefore(firstNode, `${importStatement}\n${indent}`)
98+
}
99+
47100
/** @type {import("eslint").Rule.RuleModule} */
48101
module.exports = {
49102
meta: {
@@ -54,6 +107,7 @@ module.exports = {
54107
categories: undefined,
55108
url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html'
56109
},
110+
fixable: 'code',
57111
schema: [],
58112
messages: {
59113
preferUseTemplateRef: "Replace '{{name}}' with 'useTemplateRef'."
@@ -93,6 +147,8 @@ module.exports = {
93147
}),
94148
{
95149
'Program:exit'() {
150+
let missingImportChecked = false
151+
96152
for (const templateRef of templateRefs) {
97153
const scriptRef = scriptRefs.find(
98154
(scriptRef) => scriptRef.ref === templateRef
@@ -108,6 +164,32 @@ module.exports = {
108164
data: {
109165
// @ts-ignore
110166
name: scriptRef.node?.callee?.name
167+
},
168+
fix(fixer) {
169+
/** @type {Fix[]} */
170+
const fixes = []
171+
172+
const replaceFunctionFix = fixer.replaceText(
173+
scriptRef.node,
174+
`useTemplateRef('${scriptRef.ref}')`
175+
)
176+
177+
fixes.push(replaceFunctionFix)
178+
179+
if (!missingImportChecked) {
180+
missingImportChecked = true
181+
182+
const missingImportFix = addUseTemplateRefImport(
183+
context,
184+
fixer
185+
)
186+
187+
if (missingImportFix) {
188+
fixes.push(missingImportFix)
189+
}
190+
}
191+
192+
return fixes
111193
}
112194
})
113195
}

tests/lib/rules/prefer-use-template-ref.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,15 @@ tester.run('prefer-use-template-ref', rule, {
266266
const root = ref();
267267
</script>
268268
`,
269+
output: `
270+
<template>
271+
<div ref="root"/>
272+
</template>
273+
<script setup>
274+
import { ref,useTemplateRef } from 'vue';
275+
const root = useTemplateRef('root');
276+
</script>
277+
`,
269278
errors: [
270279
{
271280
messageId: 'preferUseTemplateRef',
@@ -290,6 +299,17 @@ tester.run('prefer-use-template-ref', rule, {
290299
const link = ref();
291300
</script>
292301
`,
302+
output: `
303+
<template>
304+
<button ref="button">Content</button>
305+
<a href="" ref="link">Link</a>
306+
</template>
307+
<script setup>
308+
import { ref,useTemplateRef } from 'vue';
309+
const buttonRef = ref();
310+
const link = useTemplateRef('link');
311+
</script>
312+
`,
293313
errors: [
294314
{
295315
messageId: 'preferUseTemplateRef',
@@ -314,6 +334,17 @@ tester.run('prefer-use-template-ref', rule, {
314334
const link = ref();
315335
</script>
316336
`,
337+
output: `
338+
<template>
339+
<h1 ref="heading">Heading</h1>
340+
<a href="" ref="link">Link</a>
341+
</template>
342+
<script setup>
343+
import { ref,useTemplateRef } from 'vue';
344+
const heading = useTemplateRef('heading');
345+
const link = useTemplateRef('link');
346+
</script>
347+
`,
317348
errors: [
318349
{
319350
messageId: 'preferUseTemplateRef',
@@ -351,6 +382,22 @@ tester.run('prefer-use-template-ref', rule, {
351382
}
352383
</script>
353384
`,
385+
output: `
386+
<template>
387+
<p>Button clicked {{counter}} times.</p>
388+
<button ref="button">Click</button>
389+
</template>
390+
<script>
391+
import { ref,useTemplateRef } from 'vue';
392+
export default {
393+
name: 'Counter',
394+
setup() {
395+
const counter = ref(0);
396+
const button = useTemplateRef('button');
397+
}
398+
}
399+
</script>
400+
`,
354401
errors: [
355402
{
356403
messageId: 'preferUseTemplateRef',
@@ -373,6 +420,15 @@ tester.run('prefer-use-template-ref', rule, {
373420
const root = shallowRef();
374421
</script>
375422
`,
423+
output: `
424+
<template>
425+
<div ref="root"/>
426+
</template>
427+
<script setup>
428+
import { shallowRef,useTemplateRef } from 'vue';
429+
const root = useTemplateRef('root');
430+
</script>
431+
`,
376432
errors: [
377433
{
378434
messageId: 'preferUseTemplateRef',
@@ -383,6 +439,102 @@ tester.run('prefer-use-template-ref', rule, {
383439
column: 22
384440
}
385441
]
442+
},
443+
{
444+
filename: 'missing-import.vue',
445+
code: `
446+
<template>
447+
<p>Button clicked {{counter}} times.</p>
448+
<button ref="button">Click</button>
449+
</template>
450+
<script>
451+
import { isEqual } from 'lodash';
452+
export default {
453+
name: 'Counter',
454+
setup() {
455+
const counter = ref(0);
456+
if (isEqual(counter.value, 0)) {
457+
console.log('Counter is reset');
458+
}
459+
const button = ref();
460+
}
461+
}
462+
</script>
463+
`,
464+
output: `
465+
<template>
466+
<p>Button clicked {{counter}} times.</p>
467+
<button ref="button">Click</button>
468+
</template>
469+
<script>
470+
import { isEqual } from 'lodash';
471+
import {useTemplateRef} from 'vue';
472+
export default {
473+
name: 'Counter',
474+
setup() {
475+
const counter = ref(0);
476+
if (isEqual(counter.value, 0)) {
477+
console.log('Counter is reset');
478+
}
479+
const button = useTemplateRef('button');
480+
}
481+
}
482+
</script>
483+
`,
484+
errors: [
485+
{
486+
messageId: 'preferUseTemplateRef',
487+
data: {
488+
name: 'ref'
489+
},
490+
line: 15,
491+
column: 28
492+
}
493+
]
494+
},
495+
{
496+
filename: 'no-imports.vue',
497+
code: `
498+
<template>
499+
<p>Button clicked {{counter}} times.</p>
500+
<button ref="button">Click</button>
501+
</template>
502+
<script>
503+
export default {
504+
name: 'Counter',
505+
setup() {
506+
const counter = ref(0);
507+
const button = ref();
508+
}
509+
}
510+
</script>
511+
`,
512+
output: `
513+
<template>
514+
<p>Button clicked {{counter}} times.</p>
515+
<button ref="button">Click</button>
516+
</template>
517+
<script>
518+
import {useTemplateRef} from 'vue';
519+
export default {
520+
name: 'Counter',
521+
setup() {
522+
const counter = ref(0);
523+
const button = useTemplateRef('button');
524+
}
525+
}
526+
</script>
527+
`,
528+
errors: [
529+
{
530+
messageId: 'preferUseTemplateRef',
531+
data: {
532+
name: 'ref'
533+
},
534+
line: 11,
535+
column: 28
536+
}
537+
]
386538
}
387539
]
388540
})

0 commit comments

Comments
 (0)