no-unused-components.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /**
  2. * @fileoverview Report used components
  3. * @author Michał Sajnóg
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const utils = require('../utils')
  10. const casing = require('../utils/casing')
  11. // ------------------------------------------------------------------------------
  12. // Rule Definition
  13. // ------------------------------------------------------------------------------
  14. module.exports = {
  15. meta: {
  16. type: 'suggestion',
  17. docs: {
  18. description:
  19. 'disallow registering components that are not used inside templates',
  20. categories: ['vue3-essential', 'essential'],
  21. url: 'https://eslint.vuejs.org/rules/no-unused-components.html'
  22. },
  23. fixable: null,
  24. schema: [
  25. {
  26. type: 'object',
  27. properties: {
  28. ignoreWhenBindingPresent: {
  29. type: 'boolean'
  30. }
  31. },
  32. additionalProperties: false
  33. }
  34. ]
  35. },
  36. /** @param {RuleContext} context */
  37. create(context) {
  38. const options = context.options[0] || {}
  39. const ignoreWhenBindingPresent =
  40. options.ignoreWhenBindingPresent !== undefined
  41. ? options.ignoreWhenBindingPresent
  42. : true
  43. const usedComponents = new Set()
  44. /** @type { { node: Property, name: string }[] } */
  45. let registeredComponents = []
  46. let ignoreReporting = false
  47. /** @type {Position} */
  48. let templateLocation
  49. return utils.defineTemplateBodyVisitor(
  50. context,
  51. {
  52. /** @param {VElement} node */
  53. VElement(node) {
  54. if (
  55. (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
  56. utils.isHtmlWellKnownElementName(node.rawName) ||
  57. utils.isSvgWellKnownElementName(node.rawName)
  58. ) {
  59. return
  60. }
  61. usedComponents.add(node.rawName)
  62. },
  63. /** @param {VDirective} node */
  64. "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=true][key.name.name='is']"(
  65. node
  66. ) {
  67. if (
  68. !node.value || // `<component :is>`
  69. node.value.type !== 'VExpressionContainer' ||
  70. !node.value.expression // `<component :is="">`
  71. )
  72. return
  73. if (node.value.expression.type === 'Literal') {
  74. usedComponents.add(node.value.expression.value)
  75. } else if (ignoreWhenBindingPresent) {
  76. ignoreReporting = true
  77. }
  78. },
  79. /** @param {VAttribute} node */
  80. "VAttribute[directive=false][key.name='is']"(node) {
  81. if (!node.value) {
  82. return
  83. }
  84. const value = node.value.value.startsWith('vue:') // Usage on native elements 3.1+
  85. ? node.value.value.slice(4)
  86. : node.value.value
  87. usedComponents.add(value)
  88. },
  89. /** @param {VElement} node */
  90. "VElement[name='template']"(node) {
  91. templateLocation = templateLocation || node.loc.start
  92. },
  93. /** @param {VElement} node */
  94. "VElement[name='template']:exit"(node) {
  95. if (
  96. node.loc.start !== templateLocation ||
  97. ignoreReporting ||
  98. utils.hasAttribute(node, 'src')
  99. )
  100. return
  101. registeredComponents
  102. .filter(({ name }) => {
  103. // If the component name is PascalCase or camelCase
  104. // it can be used in various of ways inside template,
  105. // like "theComponent", "The-component" etc.
  106. // but except snake_case
  107. if (casing.isPascalCase(name) || casing.isCamelCase(name)) {
  108. return ![...usedComponents].some((n) => {
  109. return (
  110. n.indexOf('_') === -1 &&
  111. (name === casing.pascalCase(n) ||
  112. casing.camelCase(n) === name)
  113. )
  114. })
  115. } else {
  116. // In any other case the used component name must exactly match
  117. // the registered name
  118. return !usedComponents.has(name)
  119. }
  120. })
  121. .forEach(({ node, name }) =>
  122. context.report({
  123. node,
  124. message:
  125. 'The "{{name}}" component has been registered but not used.',
  126. data: {
  127. name
  128. }
  129. })
  130. )
  131. }
  132. },
  133. utils.executeOnVue(context, (obj) => {
  134. registeredComponents = utils.getRegisteredComponents(obj)
  135. })
  136. )
  137. }
  138. }