Skip to content

(implements #414) Add "no-unused-components" rule #545

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

Merged
merged 3 commits into from
Aug 13, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
| | [vue/no-side-effects-in-computed-properties](./docs/rules/no-side-effects-in-computed-properties.md) | disallow side effects in computed properties |
| | [vue/no-template-key](./docs/rules/no-template-key.md) | disallow `key` attribute on `<template>` |
| | [vue/no-textarea-mustache](./docs/rules/no-textarea-mustache.md) | disallow mustaches in `<textarea>` |
| | [vue/no-unused-components](./docs/rules/no-unused-components.md) | disallow unused components |
| | [vue/no-unused-vars](./docs/rules/no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes |
| | [vue/no-use-v-if-with-v-for](./docs/rules/no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for |
| | [vue/require-component-is](./docs/rules/require-component-is.md) | require `v-bind:is` of `<component>` elements |
Expand Down
67 changes: 67 additions & 0 deletions docs/rules/no-unused-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# disallow unused components (vue/no-unused-components)

- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.

This rule reports components that haven't been used in the template.

## :book: Rule Details

:-1: Examples of **incorrect** code for this rule:

```html
<template>
<div>
<h2>Lorem ipsum</h2>
<TheModal />
</div>
</template>

<script>
import TheButton from 'components/TheButton.vue'
import TheModal from 'components/TheModal.vue'

export default {
components: {
TheButton // Unused component
'the-modal': TheModal // Unused component
}
}
</script>
```

Note that components registered under other than `PascalCase` name have to be called directly under the specified name, whereas if you register it using `PascalCase` you can call it however you like, except using `snake_case`.

:+1: Examples of **correct** code for this rule:

```html
<template>
<div>
<h2>Lorem ipsum</h2>
<the-modal>
<component is="TheInput" />
<component :is="'TheDropdown'" />
<TheButton>CTA</TheButton>
</the-modal>
</div>
</template>

<script>
import TheButton from 'components/TheButton.vue'
import TheModal from 'components/TheModal.vue'
import TheInput from 'components/TheInput.vue'
import TheDropdown from 'components/TheDropdown.vue'

export default {
components: {
TheButton,
TheModal,
TheInput,
TheDropdown,
}
}
</script>
```

## :wrench: Options

Nothing.
1 change: 1 addition & 0 deletions lib/configs/essential.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
'vue/no-side-effects-in-computed-properties': 'error',
'vue/no-template-key': 'error',
'vue/no-textarea-mustache': 'error',
'vue/no-unused-components': 'error',
'vue/no-unused-vars': 'error',
'vue/no-use-v-if-with-v-for': 'error',
'vue/require-component-is': 'error',
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module.exports = {
'no-template-key': require('./rules/no-template-key'),
'no-template-shadow': require('./rules/no-template-shadow'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-unused-components': require('./rules/no-unused-components'),
'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-v-html': require('./rules/no-v-html'),
Expand Down
91 changes: 91 additions & 0 deletions lib/rules/no-unused-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @fileoverview Report used components
* @author Michał Sajnóg
*/
'use strict'

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

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

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

module.exports = {
meta: {
docs: {
description: 'disallow registering components that are not used inside templates',
category: 'essential',
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-unused-components.md'
},
fixable: null,
schema: []
},

create (context) {
const usedComponents = []
let registeredComponents = []
let templateLocation

return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
if (!utils.isCustomComponent(node)) return
let usedComponentName

if (utils.hasAttribute(node, 'is')) {
usedComponentName = utils.findAttribute(node, 'is').value.value
} else if (utils.hasDirective(node, 'bind', 'is')) {
const directiveNode = utils.findDirective(node, 'bind', 'is')
if (
directiveNode.value.type === 'VExpressionContainer' &&
directiveNode.value.expression.type === 'Literal'
) {
usedComponentName = directiveNode.value.expression.value
}
} else {
usedComponentName = node.rawName
}

if (usedComponentName) {
usedComponents.push(usedComponentName)
}
},
"VElement[name='template']" (rootNode) {
templateLocation = templateLocation || rootNode.loc.start
},
"VElement[name='template']:exit" (rootNode) {
if (rootNode.loc.start !== templateLocation) return

registeredComponents
.filter(({ name }) => {
// If the component name is PascalCase
// it can be used in varoious of ways inside template,
// like "theComponent", "The-component" etc.
// but except snake_case
if (casing.pascalCase(name) === name) {
return !usedComponents.some(n => {
return n.indexOf('_') === -1 && name === casing.pascalCase(n)
})
} else {
// In any other case the used component name must exactly match
// the registered name
return usedComponents.indexOf(name) === -1
}
})
.forEach(({ node, name }) => context.report({
node,
message: 'The "{{name}}" component has been registered but not used.',
data: {
name
}
}))
}
}, utils.executeOnVue(context, (obj) => {
registeredComponents = utils.getRegisteredComponents(obj)
}))
}
}
7 changes: 6 additions & 1 deletion lib/utils/casing.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,10 @@ module.exports = {
assert(typeof name === 'string')

return convertersMap[name] || pascalCase
}
},

camelCase,
pascalCase,
kebabCase,
snakeCase
}
70 changes: 59 additions & 11 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,40 +76,64 @@ module.exports = {
},

/**
* Check whether the given start tag has specific directive.
* Finds attribute in the given start tag
* @param {ASTNode} node The start tag node to check.
* @param {string} name The attribute name to check.
* @param {string} [value] The attribute value to check.
* @returns {boolean} `true` if the start tag has the directive.
* @returns {ASTNode} attribute node
*/
hasAttribute (node, name, value) {
findAttribute (node, name, value) {
assert(node && node.type === 'VElement')
return node.startTag.attributes.some(a =>
!a.directive &&
a.key.name === name &&
return node.startTag.attributes.find(attr => (
!attr.directive &&
attr.key.name === name &&
(
value === undefined ||
(a.value != null && a.value.value === value)
(attr.value != null && attr.value.value === value)
)
)
))
},

/**
* Check whether the given start tag has specific directive.
* @param {ASTNode} node The start tag node to check.
* @param {string} name The attribute name to check.
* @param {string} [value] The attribute value to check.
* @returns {boolean} `true` if the start tag has the attribute.
*/
hasAttribute (node, name, value) {
assert(node && node.type === 'VElement')
return Boolean(this.findAttribute(node, name, value))
},

/**
* Finds directive in the given start tag
* @param {ASTNode} node The start tag node to check.
* @param {string} name The directive name to check.
* @param {string} [argument] The directive argument to check.
* @returns {boolean} `true` if the start tag has the directive.
* @returns {ASTNode} directive node
*/
hasDirective (node, name, argument) {
findDirective (node, name, argument) {
assert(node && node.type === 'VElement')
return node.startTag.attributes.some(a =>
return node.startTag.attributes.find(a =>
a.directive &&
a.key.name === name &&
(argument === undefined || a.key.argument === argument)
)
},

/**
* Check whether the given start tag has specific directive.
* @param {ASTNode} node The start tag node to check.
* @param {string} name The directive name to check.
* @param {string} [argument] The directive argument to check.
* @returns {boolean} `true` if the start tag has the directive.
*/
hasDirective (node, name, argument) {
assert(node && node.type === 'VElement')
return Boolean(this.findDirective(node, name, argument))
},

/**
* Check whether the given attribute has their attribute value.
* @param {ASTNode} node The attribute node to check.
Expand Down Expand Up @@ -158,6 +182,30 @@ module.exports = {
)
},

/**
* Returns the list of all registered components
* @param {ASTNode} componentObject
* @returns {Array} Array of ASTNodes
*/
getRegisteredComponents (componentObject) {
const componentsNode = componentObject.properties
.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'components' &&
p.value.type === 'ObjectExpression'
)

if (!componentsNode) { return [] }

return componentsNode.value.properties
.filter(p => p.type === 'Property')
.map(node => ({
node,
name: this.getStaticPropertyName(node.key)
}))
},

/**
* Check whether the previous sibling element has `if` or `else-if` directive.
* @param {ASTNode} node The element node to check.
Expand Down
Loading