no-unused-components.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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. usedComponents.add(node.value.value)
  85. },
  86. /** @param {VElement} node */
  87. "VElement[name='template']"(node) {
  88. templateLocation = templateLocation || node.loc.start
  89. },
  90. /** @param {VElement} node */
  91. "VElement[name='template']:exit"(node) {
  92. if (
  93. node.loc.start !== templateLocation ||
  94. ignoreReporting ||
  95. utils.hasAttribute(node, 'src')
  96. )
  97. return
  98. registeredComponents
  99. .filter(({ name }) => {
  100. // If the component name is PascalCase or camelCase
  101. // it can be used in various of ways inside template,
  102. // like "theComponent", "The-component" etc.
  103. // but except snake_case
  104. if (casing.isPascalCase(name) || casing.isCamelCase(name)) {
  105. return ![...usedComponents].some((n) => {
  106. return (
  107. n.indexOf('_') === -1 &&
  108. (name === casing.pascalCase(n) ||
  109. casing.camelCase(n) === name)
  110. )
  111. })
  112. } else {
  113. // In any other case the used component name must exactly match
  114. // the registered name
  115. return !usedComponents.has(name)
  116. }
  117. })
  118. .forEach(({ node, name }) =>
  119. context.report({
  120. node,
  121. message:
  122. 'The "{{name}}" component has been registered but not used.',
  123. data: {
  124. name
  125. }
  126. })
  127. )
  128. }
  129. },
  130. utils.executeOnVue(context, (obj) => {
  131. registeredComponents = utils.getRegisteredComponents(obj)
  132. })
  133. )
  134. }
  135. }