diff --git a/docs/rules/README.md b/docs/rules/README.md index 89fa12b52..97a0089de 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -279,6 +279,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: | diff --git a/docs/rules/no-unregistered-components.md b/docs/rules/no-unregistered-components.md new file mode 100644 index 000000000..c90b41c41 --- /dev/null +++ b/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. +::: + + + +```vue + + + + Lorem ipsum + + + + CTA + + + + + +``` + + + + + +```vue + + + + Lorem ipsum + + + + + +``` + + + +## :wrench: Options + +```json +{ + "vue/no-unregistered-components": ["error", { + "ignorePatterns": [] + }] +} +``` + +- `ignorePatterns` Suppresses all errors if component name matches one or more patterns. + +### `ignorePatterns: ['custom(\\-\\w+)+']` + + + +```vue + + + + Lorem ipsum + + + + + +``` + + + + + +```vue + + + + Lorem ipsum + + + + + +``` + + + +## :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) diff --git a/lib/index.js b/lib/index.js index 740b49e3f..1439d91f9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -70,6 +70,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'), diff --git a/lib/rules/no-unregistered-components.js b/lib/rules/no-unregistered-components.js new file mode 100644 index 000000000..f387248ac --- /dev/null +++ b/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 || // `` + 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)) + })) + } +} diff --git a/tests/lib/rules/no-unregistered-components.js b/tests/lib/rules/no-unregistered-components.js new file mode 100644 index 000000000..2f7dd2630 --- /dev/null +++ b/tests/lib/rules/no-unregistered-components.js @@ -0,0 +1,566 @@ +/** + * @fileoverview Report used components that are not registered + * @author Jesús Ángel González Novez + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-unregistered-components') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module' + } +}) + +tester.run('no-unregistered-components', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + Lorem ipsum + + ` + }, + { + filename: 'test.vue', + code: ` + + + + `, + options: [ + { + ignorePatterns: [ + 'custom(\\-\\w+)+' + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + `, + options: [ + { + ignorePatterns: [ + 'custom(\\-\\w+)+' + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + `, + options: [ + { + ignorePatterns: [ + 'custom(\\-\\w+)+' + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + `, + options: [ + { + ignorePatterns: [ + 'Custom(\\w+)+' + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + `, + options: [ + { + ignorePatterns: [ + 'Custom(\\w+)+' + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + `, + options: [ + { + ignorePatterns: [ + 'Custom(\\w+)+' + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + Text + + + + `, + options: [ + { + ignorePatterns: [ + 'Custom(\\w+)+', + 'Warm(\\w+)+', + 'InfoBtn(\\w+)+' + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + Text + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + Text + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + Text + + + + + Text + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + + Text + + + + + Text + + + + + foo + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + }, + { + filename: 'test.vue', + code: ` + + + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + + + `, + errors: [{ + message: 'The "CustomComponent" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + + `, + options: [ + { + ignorePatterns: [ + 'custom(\\-\\w+)+' + ] + } + ], + errors: [{ + message: 'The "WarmButton" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + `, + errors: [{ + message: 'The "CustomComponent" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + `, + options: [ + { + ignorePatterns: [ + 'custom(\\-\\w+)+' + ] + } + ], + errors: [{ + message: 'The "WarmButton" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + + + `, + errors: [{ + message: 'The "CustomComponent" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + + `, + errors: [{ + message: 'The "CustomComponent" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + + + `, + errors: [{ + message: 'The "CustomComponent" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + Some text + + + + `, + errors: [{ + message: 'The "CustomComponent" component has been used but not registered.', + line: 3 + }] + }, + { + filename: 'test.vue', + code: ` + + + + + `, + errors: [{ + message: 'The "CustomComponent" component has been used but not registered.', + line: 3 + }] + } + ] +})