Skip to content

Commit

Permalink
Add vue/no-unregistered-components rule (#1114)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jesusgn90 committed May 15, 2020
1 parent b2dc044 commit a7c6696
Show file tree
Hide file tree
Showing 5 changed files with 858 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -282,6 +282,7 @@ For example:
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | |
| [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/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
Expand Down
137 changes: 137 additions & 0 deletions docs/rules/no-unregistered-components.md
@@ -0,0 +1,137 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-unregistered-components
description: disallow using components that are not registered inside templates
---
# vue/no-unregistered-components
> disallow using components that are not registered inside templates
## :book: Rule Details

This rule reports components that haven't been registered and are being used in the template.

::: warning Note
This rule cannot check globally registered components and components registered in mixins
unless you add them as part of the ignored patterns. `component`, `suspense` and `teleport`
are ignored by default.
:::

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

```vue
<!-- ✓ GOOD -->
<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>
```

</eslint-code-block>

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

```vue
<!-- ✗ BAD -->
<template>
<div>
<h2>Lorem ipsum</h2>
<TheModal />
</div>
</template>
<script>
export default {
components: {
}
}
</script>
```

</eslint-code-block>

## :wrench: Options

```json
{
"vue/no-unregistered-components": ["error", {
"ignorePatterns": []
}]
}
```

- `ignorePatterns` Suppresses all errors if component name matches one or more patterns.

### `ignorePatterns: ['custom(\\-\\w+)+']`

<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">

```vue
<!-- ✓ GOOD -->
<template>
<div>
<h2>Lorem ipsum</h2>
<CustomComponent />
</div>
</template>
<script>
export default {
components: {
},
}
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">

```vue
<!-- ✗ BAD -->
<template>
<div>
<h2>Lorem ipsum</h2>
<WarmButton />
</div>
</template>
<script>
export default {
components: {
},
}
</script>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unregistered-components.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unregistered-components.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -73,6 +73,7 @@ module.exports = {
'no-template-shadow': require('./rules/no-template-shadow'),
'no-template-target-blank': require('./rules/no-template-target-blank'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-unregistered-components': require('./rules/no-unregistered-components'),
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
'no-unused-vars': require('./rules/no-unused-vars'),
Expand Down
153 changes: 153 additions & 0 deletions lib/rules/no-unregistered-components.js
@@ -0,0 +1,153 @@
/**
* @fileoverview Report used components that are not registered
* @author Jesús Ángel González Novez
*/
'use strict'

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

const utils = require('eslint-plugin-vue/lib/utils')
const casing = require('eslint-plugin-vue/lib/utils/casing')

// ------------------------------------------------------------------------------
// Rule helpers
// ------------------------------------------------------------------------------

const VUE_BUILT_IN_COMPONENTS = [
'component',
'suspense',
'teleport',
'transition',
'transition-group',
'keep-alive',
'slot'
]
/**
* Check whether the given node is a built-in component or not.
*
* Includes `suspense` and `teleport` from Vue 3.
*
* @param {ASTNode} node The start tag node to check.
* @returns {boolean} `true` if the node is a built-in component.
*/
const isBuiltInComponent = (node) => {
const rawName = node && casing.kebabCase(node.rawName)
return utils.isHtmlElementNode(node) &&
!utils.isHtmlWellKnownElementName(node.rawName) &&
VUE_BUILT_IN_COMPONENTS.indexOf(rawName) > -1
}

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

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow using components that are not registered inside templates',
categories: null,
recommended: false,
url: 'https://eslint.vuejs.org/rules/no-unregistered-components.html'
},
fixable: null,
schema: [{
type: 'object',
properties: {
ignorePatterns: {
type: 'array'
}
},
additionalProperties: false
}]
},

create (context) {
const options = context.options[0] || {}
const ignorePatterns = options.ignorePatterns || []
const usedComponentNodes = []
const registeredComponents = []

return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
if (
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName) ||
isBuiltInComponent(node)
) {
return
}

usedComponentNodes.push({ node, name: node.rawName })
},
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']" (node) {
if (
!node.value ||
node.value.type !== 'VExpressionContainer' ||
!node.value.expression
) return

if (node.value.expression.type === 'Literal') {
if (utils.isHtmlWellKnownElementName(node.value.expression.value)) return
usedComponentNodes.push({ node, name: node.value.expression.value })
}
},
"VAttribute[directive=false][key.name='is']" (node) {
if (
!node.value || // `<component is />`
utils.isHtmlWellKnownElementName(node.value.value)
) return
usedComponentNodes.push({ node, name: node.value.value })
},
"VElement[name='template']:exit" () {
// All registered components, transformed to kebab-case
const registeredComponentNames = registeredComponents
.map(({ name }) => casing.kebabCase(name))

// All registered components using kebab-case syntax
const componentsRegisteredAsKebabCase = registeredComponents
.filter(({ name }) => name === casing.kebabCase(name))
.map(({ name }) => name)

usedComponentNodes
.filter(({ name }) => {
const kebabCaseName = casing.kebabCase(name)

// Check ignored patterns in first place
if (ignorePatterns.find(pattern => {
const regExp = new RegExp(pattern)
return regExp.test(kebabCaseName) ||
regExp.test(casing.pascalCase(name)) ||
regExp.test(casing.camelCase(name)) ||
regExp.test(casing.snakeCase(name)) ||
regExp.test(name)
})) return false

// Component registered as `foo-bar` cannot be used as `FooBar`
if (
name.indexOf('-') === -1 &&
name === casing.pascalCase(name) &&
componentsRegisteredAsKebabCase.indexOf(kebabCaseName) !== -1
) {
return true
}

// Otherwise
return registeredComponentNames.indexOf(kebabCaseName) === -1
})
.forEach(({ node, name }) => context.report({
node,
message: 'The "{{name}}" component has been used but not registered.',
data: {
name
}
}))
}
}, utils.executeOnVue(context, (obj) => {
registeredComponents.push(...utils.getRegisteredComponents(obj))
}))
}
}

0 comments on commit a7c6696

Please sign in to comment.