Skip to content

Commit 238de1b

Browse files
authored
Add vue/no-unused-refs rule (#1474)
* Add `vue/no-unused-refs` rule * update doc
1 parent 084bfe3 commit 238de1b

File tree

5 files changed

+641
-0
lines changed

5 files changed

+641
-0
lines changed

docs/rules/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ For example:
300300
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
301301
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
302302
| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty | |
303+
| [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | require valid keys in model option | |
303304
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | |
304305
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
305306
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
@@ -315,6 +316,7 @@ For example:
315316
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
316317
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
317318
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
319+
| [vue/no-unused-refs](./no-unused-refs.md) | disallow unused refs | |
318320
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
319321
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
320322
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |

docs/rules/no-unused-refs.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-unused-refs
5+
description: disallow unused refs
6+
---
7+
# vue/no-unused-refs
8+
9+
> disallow unused refs
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule is aimed at eliminating unused refs.
16+
This rule reports refs that are defined using the `ref` attribute in `<template>` but are not used via `$refs`.
17+
18+
::: warning Note
19+
This rule cannot be checked for use in other components (e.g. `mixins`, Access via `$refs.x.$refs`).
20+
:::
21+
22+
<eslint-code-block :rules="{'vue/no-unused-refs': ['error']}">
23+
24+
```vue
25+
<template>
26+
<!-- ✓ GOOD -->
27+
<input ref="foo" />
28+
29+
<!-- ✗ BAD (`bar` is not used) -->
30+
<input ref="bar" />
31+
</template>
32+
<script>
33+
export default {
34+
mounted() {
35+
this.$refs.foo.value = 'foo'
36+
}
37+
}
38+
</script>
39+
```
40+
41+
</eslint-code-block>
42+
43+
## :wrench: Options
44+
45+
Nothing.
46+
47+
## :mag: Implementation
48+
49+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unused-refs.js)
50+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unused-refs.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ module.exports = {
117117
'no-unsupported-features': require('./rules/no-unsupported-features'),
118118
'no-unused-components': require('./rules/no-unused-components'),
119119
'no-unused-properties': require('./rules/no-unused-properties'),
120+
'no-unused-refs': require('./rules/no-unused-refs'),
120121
'no-unused-vars': require('./rules/no-unused-vars'),
121122
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
122123
'no-useless-concat': require('./rules/no-useless-concat'),

lib/rules/no-unused-refs.js

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* @fileoverview Disallow unused refs.
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
17+
/**
18+
* Extract names from references objects.
19+
* @param {VReference[]} references
20+
*/
21+
function getReferences(references) {
22+
return references.filter((ref) => ref.variable == null).map((ref) => ref.id)
23+
}
24+
25+
// ------------------------------------------------------------------------------
26+
// Rule Definition
27+
// ------------------------------------------------------------------------------
28+
29+
module.exports = {
30+
meta: {
31+
type: 'suggestion',
32+
docs: {
33+
description: 'disallow unused refs',
34+
categories: undefined,
35+
url: 'https://eslint.vuejs.org/rules/no-unused-refs.html'
36+
},
37+
fixable: null,
38+
schema: [],
39+
messages: {
40+
unused: "'{{name}}' is defined as ref, but never used."
41+
}
42+
},
43+
/** @param {RuleContext} context */
44+
create(context) {
45+
/** @type {Set<string>} */
46+
const usedRefs = new Set()
47+
48+
/** @type {VLiteral[]} */
49+
const defineRefs = []
50+
let hasUnknown = false
51+
52+
/**
53+
* Report all unused refs.
54+
*/
55+
function reportUnusedRefs() {
56+
for (const defineRef of defineRefs) {
57+
if (usedRefs.has(defineRef.value)) {
58+
continue
59+
}
60+
61+
context.report({
62+
node: defineRef,
63+
messageId: 'unused',
64+
data: {
65+
name: defineRef.value
66+
}
67+
})
68+
}
69+
}
70+
71+
/**
72+
* Extract the use ref names for ObjectPattern.
73+
* @param {ObjectPattern} node
74+
* @returns {void}
75+
*/
76+
function extractUsedForObjectPattern(node) {
77+
for (const prop of node.properties) {
78+
if (prop.type === 'Property') {
79+
const name = utils.getStaticPropertyName(prop)
80+
if (name) {
81+
usedRefs.add(name)
82+
} else {
83+
hasUnknown = true
84+
return
85+
}
86+
} else {
87+
hasUnknown = true
88+
return
89+
}
90+
}
91+
}
92+
/**
93+
* Extract the use ref names.
94+
* @param {Identifier | MemberExpression} refsNode
95+
* @returns {void}
96+
*/
97+
function extractUsedForPattern(refsNode) {
98+
/** @type {Identifier | MemberExpression | ChainExpression} */
99+
let node = refsNode
100+
while (node.parent.type === 'ChainExpression') {
101+
node = node.parent
102+
}
103+
const parent = node.parent
104+
if (parent.type === 'AssignmentExpression') {
105+
if (parent.right === node) {
106+
if (parent.left.type === 'ObjectPattern') {
107+
// `({foo} = $refs)`
108+
extractUsedForObjectPattern(parent.left)
109+
} else if (parent.left.type === 'Identifier') {
110+
// `foo = $refs`
111+
hasUnknown = true
112+
}
113+
}
114+
} else if (parent.type === 'VariableDeclarator') {
115+
if (parent.init === node) {
116+
if (parent.id.type === 'ObjectPattern') {
117+
// `const {foo} = $refs`
118+
extractUsedForObjectPattern(parent.id)
119+
} else if (parent.id.type === 'Identifier') {
120+
// `const foo = $refs`
121+
hasUnknown = true
122+
}
123+
}
124+
} else if (parent.type === 'MemberExpression') {
125+
if (parent.object === node) {
126+
// `$refs.foo`
127+
const name = utils.getStaticPropertyName(parent)
128+
if (name) {
129+
usedRefs.add(name)
130+
} else {
131+
hasUnknown = true
132+
}
133+
}
134+
} else if (parent.type === 'CallExpression') {
135+
const argIndex = parent.arguments.indexOf(node)
136+
if (argIndex > -1) {
137+
// `foo($refs)`
138+
hasUnknown = true
139+
}
140+
} else if (
141+
parent.type === 'ForInStatement' ||
142+
parent.type === 'ReturnStatement'
143+
) {
144+
hasUnknown = true
145+
}
146+
}
147+
148+
return utils.defineTemplateBodyVisitor(
149+
context,
150+
{
151+
/**
152+
* @param {VExpressionContainer} node
153+
*/
154+
VExpressionContainer(node) {
155+
if (hasUnknown) {
156+
return
157+
}
158+
for (const id of getReferences(node.references)) {
159+
if (id.name !== '$refs') {
160+
continue
161+
}
162+
extractUsedForPattern(id)
163+
}
164+
},
165+
/**
166+
* @param {VAttribute} node
167+
*/
168+
'VAttribute[directive=false]'(node) {
169+
if (hasUnknown) {
170+
return
171+
}
172+
if (node.key.name === 'ref' && node.value != null) {
173+
defineRefs.push(node.value)
174+
}
175+
},
176+
"VElement[parent.type!='VElement']:exit"() {
177+
if (hasUnknown) {
178+
return
179+
}
180+
reportUnusedRefs()
181+
}
182+
},
183+
{
184+
Identifier(id) {
185+
if (hasUnknown) {
186+
return
187+
}
188+
if (id.name !== '$refs') {
189+
return
190+
}
191+
/** @type {Identifier | MemberExpression} */
192+
let refsNode = id
193+
if (id.parent.type === 'MemberExpression') {
194+
if (id.parent.property === id) {
195+
// `this.$refs.foo`
196+
refsNode = id.parent
197+
}
198+
}
199+
extractUsedForPattern(refsNode)
200+
}
201+
}
202+
)
203+
}
204+
}

0 commit comments

Comments
 (0)