Skip to content

Commit 8673fc3

Browse files
authored
(implements #414) Add "no-unused-components" rule (#545)
* Add "no-unused-components" rule * Handle case with Literal in :is binding expression, update docs * Update no-unused-components.md
1 parent 52e0462 commit 8673fc3

File tree

9 files changed

+634
-12
lines changed

9 files changed

+634
-12
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
156156
| | [vue/no-side-effects-in-computed-properties](./docs/rules/no-side-effects-in-computed-properties.md) | disallow side effects in computed properties |
157157
| | [vue/no-template-key](./docs/rules/no-template-key.md) | disallow `key` attribute on `<template>` |
158158
| | [vue/no-textarea-mustache](./docs/rules/no-textarea-mustache.md) | disallow mustaches in `<textarea>` |
159+
| | [vue/no-unused-components](./docs/rules/no-unused-components.md) | disallow unused components |
159160
| | [vue/no-unused-vars](./docs/rules/no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes |
160161
| | [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 |
161162
| | [vue/require-component-is](./docs/rules/require-component-is.md) | require `v-bind:is` of `<component>` elements |

docs/rules/no-unused-components.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# disallow unused components (vue/no-unused-components)
2+
3+
- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
4+
5+
This rule reports components that haven't been used in the template.
6+
7+
## :book: Rule Details
8+
9+
:-1: Examples of **incorrect** code for this rule:
10+
11+
```html
12+
<template>
13+
<div>
14+
<h2>Lorem ipsum</h2>
15+
<TheModal />
16+
</div>
17+
</template>
18+
19+
<script>
20+
import TheButton from 'components/TheButton.vue'
21+
import TheModal from 'components/TheModal.vue'
22+
23+
export default {
24+
components: {
25+
TheButton // Unused component
26+
'the-modal': TheModal // Unused component
27+
}
28+
}
29+
</script>
30+
```
31+
32+
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`.
33+
34+
:+1: Examples of **correct** code for this rule:
35+
36+
```html
37+
<template>
38+
<div>
39+
<h2>Lorem ipsum</h2>
40+
<the-modal>
41+
<component is="TheInput" />
42+
<component :is="'TheDropdown'" />
43+
<TheButton>CTA</TheButton>
44+
</the-modal>
45+
</div>
46+
</template>
47+
48+
<script>
49+
import TheButton from 'components/TheButton.vue'
50+
import TheModal from 'components/TheModal.vue'
51+
import TheInput from 'components/TheInput.vue'
52+
import TheDropdown from 'components/TheDropdown.vue'
53+
54+
export default {
55+
components: {
56+
TheButton,
57+
TheModal,
58+
TheInput,
59+
TheDropdown,
60+
}
61+
}
62+
</script>
63+
```
64+
65+
## :wrench: Options
66+
67+
Nothing.

lib/configs/essential.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = {
1515
'vue/no-side-effects-in-computed-properties': 'error',
1616
'vue/no-template-key': 'error',
1717
'vue/no-textarea-mustache': 'error',
18+
'vue/no-unused-components': 'error',
1819
'vue/no-unused-vars': 'error',
1920
'vue/no-use-v-if-with-v-for': 'error',
2021
'vue/require-component-is': 'error',

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module.exports = {
3333
'no-template-key': require('./rules/no-template-key'),
3434
'no-template-shadow': require('./rules/no-template-shadow'),
3535
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
36+
'no-unused-components': require('./rules/no-unused-components'),
3637
'no-unused-vars': require('./rules/no-unused-vars'),
3738
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
3839
'no-v-html': require('./rules/no-v-html'),

lib/rules/no-unused-components.js

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* @fileoverview Report used components
3+
* @author Michał Sajnóg
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
const casing = require('../utils/casing')
13+
14+
// ------------------------------------------------------------------------------
15+
// Rule Definition
16+
// ------------------------------------------------------------------------------
17+
18+
module.exports = {
19+
meta: {
20+
docs: {
21+
description: 'disallow registering components that are not used inside templates',
22+
category: 'essential',
23+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.1/docs/rules/no-unused-components.md'
24+
},
25+
fixable: null,
26+
schema: []
27+
},
28+
29+
create (context) {
30+
const usedComponents = []
31+
let registeredComponents = []
32+
let templateLocation
33+
34+
return utils.defineTemplateBodyVisitor(context, {
35+
VElement (node) {
36+
if (!utils.isCustomComponent(node)) return
37+
let usedComponentName
38+
39+
if (utils.hasAttribute(node, 'is')) {
40+
usedComponentName = utils.findAttribute(node, 'is').value.value
41+
} else if (utils.hasDirective(node, 'bind', 'is')) {
42+
const directiveNode = utils.findDirective(node, 'bind', 'is')
43+
if (
44+
directiveNode.value.type === 'VExpressionContainer' &&
45+
directiveNode.value.expression.type === 'Literal'
46+
) {
47+
usedComponentName = directiveNode.value.expression.value
48+
}
49+
} else {
50+
usedComponentName = node.rawName
51+
}
52+
53+
if (usedComponentName) {
54+
usedComponents.push(usedComponentName)
55+
}
56+
},
57+
"VElement[name='template']" (rootNode) {
58+
templateLocation = templateLocation || rootNode.loc.start
59+
},
60+
"VElement[name='template']:exit" (rootNode) {
61+
if (rootNode.loc.start !== templateLocation) return
62+
63+
registeredComponents
64+
.filter(({ name }) => {
65+
// If the component name is PascalCase
66+
// it can be used in varoious of ways inside template,
67+
// like "theComponent", "The-component" etc.
68+
// but except snake_case
69+
if (casing.pascalCase(name) === name) {
70+
return !usedComponents.some(n => {
71+
return n.indexOf('_') === -1 && name === casing.pascalCase(n)
72+
})
73+
} else {
74+
// In any other case the used component name must exactly match
75+
// the registered name
76+
return usedComponents.indexOf(name) === -1
77+
}
78+
})
79+
.forEach(({ node, name }) => context.report({
80+
node,
81+
message: 'The "{{name}}" component has been registered but not used.',
82+
data: {
83+
name
84+
}
85+
}))
86+
}
87+
}, utils.executeOnVue(context, (obj) => {
88+
registeredComponents = utils.getRegisteredComponents(obj)
89+
}))
90+
}
91+
}

lib/utils/casing.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,10 @@ module.exports = {
7575
assert(typeof name === 'string')
7676

7777
return convertersMap[name] || pascalCase
78-
}
78+
},
79+
80+
camelCase,
81+
pascalCase,
82+
kebabCase,
83+
snakeCase
7984
}

lib/utils/index.js

+59-11
Original file line numberDiff line numberDiff line change
@@ -76,40 +76,64 @@ module.exports = {
7676
},
7777

7878
/**
79-
* Check whether the given start tag has specific directive.
79+
* Finds attribute in the given start tag
8080
* @param {ASTNode} node The start tag node to check.
8181
* @param {string} name The attribute name to check.
8282
* @param {string} [value] The attribute value to check.
83-
* @returns {boolean} `true` if the start tag has the directive.
83+
* @returns {ASTNode} attribute node
8484
*/
85-
hasAttribute (node, name, value) {
85+
findAttribute (node, name, value) {
8686
assert(node && node.type === 'VElement')
87-
return node.startTag.attributes.some(a =>
88-
!a.directive &&
89-
a.key.name === name &&
87+
return node.startTag.attributes.find(attr => (
88+
!attr.directive &&
89+
attr.key.name === name &&
9090
(
9191
value === undefined ||
92-
(a.value != null && a.value.value === value)
92+
(attr.value != null && attr.value.value === value)
9393
)
94-
)
94+
))
9595
},
9696

9797
/**
9898
* Check whether the given start tag has specific directive.
9999
* @param {ASTNode} node The start tag node to check.
100+
* @param {string} name The attribute name to check.
101+
* @param {string} [value] The attribute value to check.
102+
* @returns {boolean} `true` if the start tag has the attribute.
103+
*/
104+
hasAttribute (node, name, value) {
105+
assert(node && node.type === 'VElement')
106+
return Boolean(this.findAttribute(node, name, value))
107+
},
108+
109+
/**
110+
* Finds directive in the given start tag
111+
* @param {ASTNode} node The start tag node to check.
100112
* @param {string} name The directive name to check.
101113
* @param {string} [argument] The directive argument to check.
102-
* @returns {boolean} `true` if the start tag has the directive.
114+
* @returns {ASTNode} directive node
103115
*/
104-
hasDirective (node, name, argument) {
116+
findDirective (node, name, argument) {
105117
assert(node && node.type === 'VElement')
106-
return node.startTag.attributes.some(a =>
118+
return node.startTag.attributes.find(a =>
107119
a.directive &&
108120
a.key.name === name &&
109121
(argument === undefined || a.key.argument === argument)
110122
)
111123
},
112124

125+
/**
126+
* Check whether the given start tag has specific directive.
127+
* @param {ASTNode} node The start tag node to check.
128+
* @param {string} name The directive name to check.
129+
* @param {string} [argument] The directive argument to check.
130+
* @returns {boolean} `true` if the start tag has the directive.
131+
*/
132+
hasDirective (node, name, argument) {
133+
assert(node && node.type === 'VElement')
134+
return Boolean(this.findDirective(node, name, argument))
135+
},
136+
113137
/**
114138
* Check whether the given attribute has their attribute value.
115139
* @param {ASTNode} node The attribute node to check.
@@ -158,6 +182,30 @@ module.exports = {
158182
)
159183
},
160184

185+
/**
186+
* Returns the list of all registered components
187+
* @param {ASTNode} componentObject
188+
* @returns {Array} Array of ASTNodes
189+
*/
190+
getRegisteredComponents (componentObject) {
191+
const componentsNode = componentObject.properties
192+
.find(p =>
193+
p.type === 'Property' &&
194+
p.key.type === 'Identifier' &&
195+
p.key.name === 'components' &&
196+
p.value.type === 'ObjectExpression'
197+
)
198+
199+
if (!componentsNode) { return [] }
200+
201+
return componentsNode.value.properties
202+
.filter(p => p.type === 'Property')
203+
.map(node => ({
204+
node,
205+
name: this.getStaticPropertyName(node.key)
206+
}))
207+
},
208+
161209
/**
162210
* Check whether the previous sibling element has `if` or `else-if` directive.
163211
* @param {ASTNode} node The element node to check.

0 commit comments

Comments
 (0)