Skip to content

Commit a7c6696

Browse files
author
Jesús Ángel
authored
Add vue/no-unregistered-components rule (#1114)
* Add `vue/no-unregistered-components` rule * Remove rule from configs/essential Also remove text about where rule is included * Extend allowed ignore patterns * Add note about globally/mixins registered components + use strings for ingore patterns * (auto) update rules index * (auto) update rules lib index * Fix PR review's concerns - Correct rule categories (null) - Ignore `component :is="..."` component for well known HTML tags. - Ignore `component`, `suspense`, `teleport` as unknown components. * Add more rule tests to cover latest changes * Correct + add test for `suspense` and `teleport` * Restore not intended change in package.json * Progress with PR review * Handle edge case `<component is="div" />` * Progress with PR review - Remove no longer needed variable. - Handle edge case where a component is registered using kebab-case but later on is used using PascalCase. e.g: registered as `foo-bar` and used as `FooBar` is not valid. - Handle edge case `<component is />`, where `node.value` would be `null`. See https://github.com/mysticatea/vue-eslint-parser/blob/master/docs/ast.md#vattribute - Add more tests to prove all these changes * Remove unused block
1 parent b2dc044 commit a7c6696

File tree

5 files changed

+858
-0
lines changed

5 files changed

+858
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ For example:
282282
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
283283
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
284284
| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | |
285+
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
285286
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
286287
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
287288
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |

Diff for: docs/rules/no-unregistered-components.md

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-unregistered-components
5+
description: disallow using components that are not registered inside templates
6+
---
7+
# vue/no-unregistered-components
8+
> disallow using components that are not registered inside templates
9+
10+
## :book: Rule Details
11+
12+
This rule reports components that haven't been registered and are being used in the template.
13+
14+
::: warning Note
15+
This rule cannot check globally registered components and components registered in mixins
16+
unless you add them as part of the ignored patterns. `component`, `suspense` and `teleport`
17+
are ignored by default.
18+
:::
19+
20+
<eslint-code-block :rules="{'vue/no-unregistered-components': ['error']}">
21+
22+
```vue
23+
<!-- ✓ GOOD -->
24+
<template>
25+
<div>
26+
<h2>Lorem ipsum</h2>
27+
<the-modal>
28+
<component is="TheInput" />
29+
<component :is="'TheDropdown'" />
30+
<TheButton>CTA</TheButton>
31+
</the-modal>
32+
</div>
33+
</template>
34+
35+
<script>
36+
import TheButton from 'components/TheButton.vue'
37+
import TheModal from 'components/TheModal.vue'
38+
import TheInput from 'components/TheInput.vue'
39+
import TheDropdown from 'components/TheDropdown.vue'
40+
41+
export default {
42+
components: {
43+
TheButton,
44+
TheModal,
45+
TheInput,
46+
TheDropdown,
47+
}
48+
}
49+
</script>
50+
```
51+
52+
</eslint-code-block>
53+
54+
<eslint-code-block :rules="{'vue/no-unregistered-components': ['error']}">
55+
56+
```vue
57+
<!-- ✗ BAD -->
58+
<template>
59+
<div>
60+
<h2>Lorem ipsum</h2>
61+
<TheModal />
62+
</div>
63+
</template>
64+
65+
<script>
66+
export default {
67+
components: {
68+
69+
}
70+
}
71+
</script>
72+
```
73+
74+
</eslint-code-block>
75+
76+
## :wrench: Options
77+
78+
```json
79+
{
80+
"vue/no-unregistered-components": ["error", {
81+
"ignorePatterns": []
82+
}]
83+
}
84+
```
85+
86+
- `ignorePatterns` Suppresses all errors if component name matches one or more patterns.
87+
88+
### `ignorePatterns: ['custom(\\-\\w+)+']`
89+
90+
<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">
91+
92+
```vue
93+
<!-- ✓ GOOD -->
94+
<template>
95+
<div>
96+
<h2>Lorem ipsum</h2>
97+
<CustomComponent />
98+
</div>
99+
</template>
100+
101+
<script>
102+
export default {
103+
components: {
104+
105+
},
106+
}
107+
</script>
108+
```
109+
110+
</eslint-code-block>
111+
112+
<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">
113+
114+
```vue
115+
<!-- ✗ BAD -->
116+
<template>
117+
<div>
118+
<h2>Lorem ipsum</h2>
119+
<WarmButton />
120+
</div>
121+
</template>
122+
123+
<script>
124+
export default {
125+
components: {
126+
127+
},
128+
}
129+
</script>
130+
```
131+
132+
</eslint-code-block>
133+
134+
## :mag: Implementation
135+
136+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unregistered-components.js)
137+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unregistered-components.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = {
7373
'no-template-shadow': require('./rules/no-template-shadow'),
7474
'no-template-target-blank': require('./rules/no-template-target-blank'),
7575
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
76+
'no-unregistered-components': require('./rules/no-unregistered-components'),
7677
'no-unsupported-features': require('./rules/no-unsupported-features'),
7778
'no-unused-components': require('./rules/no-unused-components'),
7879
'no-unused-vars': require('./rules/no-unused-vars'),

Diff for: lib/rules/no-unregistered-components.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @fileoverview Report used components that are not registered
3+
* @author Jesús Ángel González Novez
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('eslint-plugin-vue/lib/utils')
12+
const casing = require('eslint-plugin-vue/lib/utils/casing')
13+
14+
// ------------------------------------------------------------------------------
15+
// Rule helpers
16+
// ------------------------------------------------------------------------------
17+
18+
const VUE_BUILT_IN_COMPONENTS = [
19+
'component',
20+
'suspense',
21+
'teleport',
22+
'transition',
23+
'transition-group',
24+
'keep-alive',
25+
'slot'
26+
]
27+
/**
28+
* Check whether the given node is a built-in component or not.
29+
*
30+
* Includes `suspense` and `teleport` from Vue 3.
31+
*
32+
* @param {ASTNode} node The start tag node to check.
33+
* @returns {boolean} `true` if the node is a built-in component.
34+
*/
35+
const isBuiltInComponent = (node) => {
36+
const rawName = node && casing.kebabCase(node.rawName)
37+
return utils.isHtmlElementNode(node) &&
38+
!utils.isHtmlWellKnownElementName(node.rawName) &&
39+
VUE_BUILT_IN_COMPONENTS.indexOf(rawName) > -1
40+
}
41+
42+
// ------------------------------------------------------------------------------
43+
// Rule Definition
44+
// ------------------------------------------------------------------------------
45+
46+
module.exports = {
47+
meta: {
48+
type: 'suggestion',
49+
docs: {
50+
description: 'disallow using components that are not registered inside templates',
51+
categories: null,
52+
recommended: false,
53+
url: 'https://eslint.vuejs.org/rules/no-unregistered-components.html'
54+
},
55+
fixable: null,
56+
schema: [{
57+
type: 'object',
58+
properties: {
59+
ignorePatterns: {
60+
type: 'array'
61+
}
62+
},
63+
additionalProperties: false
64+
}]
65+
},
66+
67+
create (context) {
68+
const options = context.options[0] || {}
69+
const ignorePatterns = options.ignorePatterns || []
70+
const usedComponentNodes = []
71+
const registeredComponents = []
72+
73+
return utils.defineTemplateBodyVisitor(context, {
74+
VElement (node) {
75+
if (
76+
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
77+
utils.isHtmlWellKnownElementName(node.rawName) ||
78+
utils.isSvgWellKnownElementName(node.rawName) ||
79+
isBuiltInComponent(node)
80+
) {
81+
return
82+
}
83+
84+
usedComponentNodes.push({ node, name: node.rawName })
85+
},
86+
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']" (node) {
87+
if (
88+
!node.value ||
89+
node.value.type !== 'VExpressionContainer' ||
90+
!node.value.expression
91+
) return
92+
93+
if (node.value.expression.type === 'Literal') {
94+
if (utils.isHtmlWellKnownElementName(node.value.expression.value)) return
95+
usedComponentNodes.push({ node, name: node.value.expression.value })
96+
}
97+
},
98+
"VAttribute[directive=false][key.name='is']" (node) {
99+
if (
100+
!node.value || // `<component is />`
101+
utils.isHtmlWellKnownElementName(node.value.value)
102+
) return
103+
usedComponentNodes.push({ node, name: node.value.value })
104+
},
105+
"VElement[name='template']:exit" () {
106+
// All registered components, transformed to kebab-case
107+
const registeredComponentNames = registeredComponents
108+
.map(({ name }) => casing.kebabCase(name))
109+
110+
// All registered components using kebab-case syntax
111+
const componentsRegisteredAsKebabCase = registeredComponents
112+
.filter(({ name }) => name === casing.kebabCase(name))
113+
.map(({ name }) => name)
114+
115+
usedComponentNodes
116+
.filter(({ name }) => {
117+
const kebabCaseName = casing.kebabCase(name)
118+
119+
// Check ignored patterns in first place
120+
if (ignorePatterns.find(pattern => {
121+
const regExp = new RegExp(pattern)
122+
return regExp.test(kebabCaseName) ||
123+
regExp.test(casing.pascalCase(name)) ||
124+
regExp.test(casing.camelCase(name)) ||
125+
regExp.test(casing.snakeCase(name)) ||
126+
regExp.test(name)
127+
})) return false
128+
129+
// Component registered as `foo-bar` cannot be used as `FooBar`
130+
if (
131+
name.indexOf('-') === -1 &&
132+
name === casing.pascalCase(name) &&
133+
componentsRegisteredAsKebabCase.indexOf(kebabCaseName) !== -1
134+
) {
135+
return true
136+
}
137+
138+
// Otherwise
139+
return registeredComponentNames.indexOf(kebabCaseName) === -1
140+
})
141+
.forEach(({ node, name }) => context.report({
142+
node,
143+
message: 'The "{{name}}" component has been used but not registered.',
144+
data: {
145+
name
146+
}
147+
}))
148+
}
149+
}, utils.executeOnVue(context, (obj) => {
150+
registeredComponents.push(...utils.getRegisteredComponents(obj))
151+
}))
152+
}
153+
}

0 commit comments

Comments
 (0)