37ecba1c738176995cd2adf49fc79649cd0a004013b974a6cfae7f87776716cc87d8439cbbdbfeda74c0923439341386422abebe91de30dc23926bb97d3fdf 6.1 KB

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