Skip to content
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

Add vue/no-unused-refs rule #1474

Merged
merged 2 commits into from Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/rules/README.md
Expand Up @@ -300,6 +300,7 @@ For example:
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
| [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` | |
| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty | |
| [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | require valid keys in model option | |
| [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class | |
| [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | |
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
Expand All @@ -315,6 +316,7 @@ For example:
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | |
| [vue/no-unused-refs](./no-unused-refs.md) | disallow unused refs | |
| [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: |
| [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
Expand Down
50 changes: 50 additions & 0 deletions docs/rules/no-unused-refs.md
@@ -0,0 +1,50 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-unused-refs
description: disallow unused refs
---
# vue/no-unused-refs

> disallow unused 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

This rule is aimed at eliminating unused refs.
This rule reports refs that are defined using the `ref` attribute in `<template>` but are not used via `$refs`.

::: warning Note
This rule cannot be checked for use in other components (e.g. `mixins`, Access via `$refs.x.$refs`).
:::

<eslint-code-block :rules="{'vue/no-unused-refs': ['error']}">

```vue
<template>
<!-- ✓ GOOD -->
<input ref="foo" />

<!-- ✗ BAD (`bar` is not used) -->
<input ref="bar" />
</template>
<script>
export default {
mounted() {
this.$refs.foo.value = 'foo'
}
}
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unused-refs.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unused-refs.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -117,6 +117,7 @@ module.exports = {
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
'no-unused-properties': require('./rules/no-unused-properties'),
'no-unused-refs': require('./rules/no-unused-refs'),
'no-unused-vars': require('./rules/no-unused-vars'),
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
'no-useless-concat': require('./rules/no-useless-concat'),
Expand Down
204 changes: 204 additions & 0 deletions lib/rules/no-unused-refs.js
@@ -0,0 +1,204 @@
/**
* @fileoverview Disallow unused refs.
* @author Yosuke Ota
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

/**
* Extract names from references objects.
* @param {VReference[]} references
*/
function getReferences(references) {
return references.filter((ref) => ref.variable == null).map((ref) => ref.id)
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow unused refs',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/no-unused-refs.html'
},
fixable: null,
schema: [],
messages: {
unused: "'{{name}}' is defined as ref, but never used."
}
},
/** @param {RuleContext} context */
create(context) {
/** @type {Set<string>} */
const usedRefs = new Set()

/** @type {VLiteral[]} */
const defineRefs = []
let hasUnknown = false

/**
* Report all unused refs.
*/
function reportUnusedRefs() {
for (const defineRef of defineRefs) {
if (usedRefs.has(defineRef.value)) {
continue
}

context.report({
node: defineRef,
messageId: 'unused',
data: {
name: defineRef.value
}
})
}
}

/**
* Extract the use ref names for ObjectPattern.
* @param {ObjectPattern} node
* @returns {void}
*/
function extractUsedForObjectPattern(node) {
for (const prop of node.properties) {
if (prop.type === 'Property') {
const name = utils.getStaticPropertyName(prop)
if (name) {
usedRefs.add(name)
} else {
hasUnknown = true
return
}
} else {
hasUnknown = true
return
}
}
}
/**
* Extract the use ref names.
* @param {Identifier | MemberExpression} refsNode
* @returns {void}
*/
function extractUsedForPattern(refsNode) {
/** @type {Identifier | MemberExpression | ChainExpression} */
let node = refsNode
while (node.parent.type === 'ChainExpression') {
node = node.parent
}
const parent = node.parent
if (parent.type === 'AssignmentExpression') {
if (parent.right === node) {
if (parent.left.type === 'ObjectPattern') {
// `({foo} = $refs)`
extractUsedForObjectPattern(parent.left)
} else if (parent.left.type === 'Identifier') {
// `foo = $refs`
hasUnknown = true
}
}
} else if (parent.type === 'VariableDeclarator') {
if (parent.init === node) {
if (parent.id.type === 'ObjectPattern') {
// `const {foo} = $refs`
extractUsedForObjectPattern(parent.id)
} else if (parent.id.type === 'Identifier') {
// `const foo = $refs`
hasUnknown = true
}
}
} else if (parent.type === 'MemberExpression') {
if (parent.object === node) {
// `$refs.foo`
const name = utils.getStaticPropertyName(parent)
if (name) {
usedRefs.add(name)
} else {
hasUnknown = true
}
}
} else if (parent.type === 'CallExpression') {
const argIndex = parent.arguments.indexOf(node)
if (argIndex > -1) {
// `foo($refs)`
hasUnknown = true
}
} else if (
parent.type === 'ForInStatement' ||
parent.type === 'ReturnStatement'
) {
hasUnknown = true
}
}

return utils.defineTemplateBodyVisitor(
context,
{
/**
* @param {VExpressionContainer} node
*/
VExpressionContainer(node) {
if (hasUnknown) {
return
}
for (const id of getReferences(node.references)) {
if (id.name !== '$refs') {
continue
}
extractUsedForPattern(id)
}
},
/**
* @param {VAttribute} node
*/
'VAttribute[directive=false]'(node) {
if (hasUnknown) {
return
}
if (node.key.name === 'ref' && node.value != null) {
defineRefs.push(node.value)
}
},
"VElement[parent.type!='VElement']:exit"() {
if (hasUnknown) {
return
}
reportUnusedRefs()
}
},
{
Identifier(id) {
if (hasUnknown) {
return
}
if (id.name !== '$refs') {
return
}
/** @type {Identifier | MemberExpression} */
let refsNode = id
if (id.parent.type === 'MemberExpression') {
if (id.parent.property === id) {
// `this.$refs.foo`
refsNode = id.parent
}
}
extractUsedForPattern(refsNode)
}
}
)
}
}