Skip to content

Commit 2049c5c

Browse files
authored
Fix #556: (no-unused-component) Fix component detection, add "ignoreWhenBindingPresent" option (#558)
* Fix custom component detection * Add ignoreWhenBindingPresent setting to no-unused-components * Make ignoreWhenBindingPresent setting true by default in no-unused-components rule
1 parent 3f5d41e commit 2049c5c

File tree

4 files changed

+198
-23
lines changed

4 files changed

+198
-23
lines changed

docs/rules/no-unused-components.md

+82-1
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,85 @@ Note that components registered under other than `PascalCase` name have to be ca
6464

6565
## :wrench: Options
6666

67-
Nothing.
67+
```json
68+
{
69+
"vue/no-unused-components": ["error", {
70+
"ignoreWhenBindingPresent": true
71+
}]
72+
}
73+
```
74+
75+
- `ignoreWhenBindingPresent` ... surpresses all errors if binding has been detected in the template
76+
default `true`
77+
78+
79+
:+1: Examples of **incorrect** code:
80+
81+
```json
82+
{
83+
"vue/no-unused-components": ["error", {
84+
"ignoreWhenBindingPresent": false
85+
}]
86+
}
87+
```
88+
89+
```html
90+
<template>
91+
<div>
92+
<h2>Lorem ipsum</h2>
93+
<component :is="computedComponent" />
94+
</div>
95+
</template>
96+
97+
<script>
98+
import TheButton from 'components/TheButton.vue'
99+
import TheSelect from 'components/TheSelect.vue'
100+
import TheInput from 'components/TheInput.vue'
101+
102+
export default {
103+
components: {
104+
TheButton, // <- not used
105+
TheSelect, // <- not used
106+
TheInput, // <- not used
107+
},
108+
computed: {
109+
computedComponent() { ... }
110+
}
111+
}
112+
</script>
113+
```
114+
115+
:+1: Examples of **correct** code:
116+
117+
```json
118+
{
119+
"vue/no-unused-components": ["error", {
120+
"ignoreWhenBindingPresent": false
121+
}]
122+
}
123+
```
124+
125+
```html
126+
<template>
127+
<div>
128+
<h2>Lorem ipsum</h2>
129+
<TheButton v-if="" />
130+
<TheSelect v-else-if="" />
131+
<TheInput v-else="" />
132+
</div>
133+
</template>
134+
135+
<script>
136+
import TheButton from 'components/TheButton.vue'
137+
import TheSelect from 'components/TheSelect.vue'
138+
import TheInput from 'components/TheInput.vue'
139+
140+
export default {
141+
components: {
142+
TheButton,
143+
TheSelect,
144+
TheInput,
145+
},
146+
}
147+
</script>
148+
```

lib/rules/no-unused-components.js

+28-19
Original file line numberDiff line numberDiff line change
@@ -23,42 +23,51 @@ module.exports = {
2323
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/no-unused-components.md'
2424
},
2525
fixable: null,
26-
schema: []
26+
schema: [{
27+
type: 'object',
28+
properties: {
29+
ignoreWhenBindingPresent: {
30+
type: 'boolean'
31+
}
32+
},
33+
additionalProperties: false
34+
}]
2735
},
2836

2937
create (context) {
38+
const options = context.options[0] || {}
39+
const ignoreWhenBindingPresent = options.ignoreWhenBindingPresent !== undefined ? options.ignoreWhenBindingPresent : true
3040
const usedComponents = []
3141
let registeredComponents = []
42+
let ignoreReporting = false
3243
let templateLocation
3344

3445
return utils.defineTemplateBodyVisitor(context, {
3546
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
47+
if (utils.isHtmlElementNode(node) && !utils.isHtmlWellKnownElementName(node.rawName)) {
48+
usedComponents.push(node.rawName)
5149
}
50+
},
51+
"VAttribute[directive=true][key.name='bind'][key.argument='is']" (node) {
52+
if (node.value.type !== 'VExpressionContainer') return
5253

53-
if (usedComponentName) {
54-
usedComponents.push(usedComponentName)
54+
if (node.value.expression.type === 'Literal') {
55+
usedComponents.push(node.value.expression.value)
56+
} else if (ignoreWhenBindingPresent) {
57+
ignoreReporting = true
5558
}
5659
},
60+
"VAttribute[directive=false][key.name='is']" (node) {
61+
usedComponents.push(node.value.value)
62+
},
5763
"VElement[name='template']" (rootNode) {
5864
templateLocation = templateLocation || rootNode.loc.start
5965
},
6066
"VElement[name='template']:exit" (rootNode) {
61-
if (rootNode.loc.start !== templateLocation) return
67+
if (
68+
rootNode.loc.start !== templateLocation ||
69+
ignoreReporting
70+
) return
6271

6372
registeredComponents
6473
.filter(({ name }) => {

lib/utils/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ module.exports = {
233233
assert(node && node.type === 'VElement')
234234

235235
return (
236-
(this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) ||
236+
(this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.rawName)) ||
237237
this.hasAttribute(node, 'is') ||
238238
this.hasDirective(node, 'bind', 'is')
239239
)
@@ -280,7 +280,7 @@ module.exports = {
280280
isHtmlWellKnownElementName (name) {
281281
assert(typeof name === 'string')
282282

283-
return HTML_ELEMENT_NAMES.has(name.toLowerCase())
283+
return HTML_ELEMENT_NAMES.has(name)
284284
},
285285

286286
/**
@@ -291,7 +291,7 @@ module.exports = {
291291
isHtmlVoidElementName (name) {
292292
assert(typeof name === 'string')
293293

294-
return VOID_ELEMENT_NAMES.has(name.toLowerCase())
294+
return VOID_ELEMENT_NAMES.has(name)
295295
},
296296

297297
/**

tests/lib/rules/no-unused-components.js

+85
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,57 @@ tester.run('no-unused-components', rule, {
282282
}
283283
</script>`
284284
},
285+
{
286+
filename: 'test.vue',
287+
code: `<template>
288+
<div id="app">
289+
<Header></Header>
290+
<div class="content">
291+
<router-view></router-view>
292+
</div>
293+
<Footer />
294+
</div>
295+
</template>
296+
<script>
297+
import Header from 'components/Layout/Header';
298+
import Footer from 'components/Layout/Footer';
299+
300+
export default {
301+
name: 'App',
302+
components: {
303+
Header,
304+
Footer,
305+
},
306+
};
307+
</script>`
308+
},
309+
310+
{
311+
filename: 'test.vue',
312+
code: `
313+
<template>
314+
<div>
315+
<component :is="dynamicComponent"></component>
316+
</div>
317+
</template>
318+
<script>
319+
import Foo from 'components/Foo';
320+
import Bar from 'components/Bar';
321+
322+
export default {
323+
components: {
324+
Foo,
325+
Bar
326+
},
327+
computed: {
328+
dynamicComponent() {
329+
return '...'
330+
}
331+
}
332+
}
333+
</script>`,
334+
options: [{ ignoreWhenBindingPresent: true }]
335+
},
285336

286337
// Ignore when `render` is used instead of temoplate
287338
{
@@ -364,6 +415,40 @@ tester.run('no-unused-components', rule, {
364415
message: 'The "the-button" component has been registered but not used.',
365416
line: 11
366417
}]
418+
},
419+
// Setting: ignoreWhenBindingPresent
420+
{
421+
filename: 'test.vue',
422+
code: `
423+
<template>
424+
<div>
425+
<component :is="dynamicComponent"></component>
426+
</div>
427+
</template>
428+
<script>
429+
import Foo from 'components/Foo';
430+
import Bar from 'components/Bar';
431+
432+
export default {
433+
components: {
434+
Foo,
435+
Bar
436+
},
437+
computed: {
438+
dynamicComponent() {
439+
return '...'
440+
}
441+
}
442+
}
443+
</script>`,
444+
options: [{ ignoreWhenBindingPresent: false }],
445+
errors: [{
446+
message: 'The "Foo" component has been registered but not used.',
447+
line: 13
448+
}, {
449+
message: 'The "Bar" component has been registered but not used.',
450+
line: 14
451+
}]
367452
}
368453
]
369454
})

0 commit comments

Comments
 (0)