no-undef-components.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const path = require('path')
  7. const utils = require('../utils')
  8. const casing = require('../utils/casing')
  9. /**
  10. * `casing.camelCase()` converts the beginning to lowercase,
  11. * but does not convert the case of the beginning character when converting with Vue3.
  12. * @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/shared/src/index.ts#L105
  13. * @param {string} str
  14. */
  15. function camelize(str) {
  16. return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
  17. }
  18. class DefinedInSetupComponents {
  19. constructor() {
  20. /**
  21. * Component names
  22. * @type {Set<string>}
  23. */
  24. this.names = new Set()
  25. }
  26. /**
  27. * @param {string[]} names
  28. */
  29. addName(...names) {
  30. for (const name of names) {
  31. this.names.add(name)
  32. }
  33. }
  34. /**
  35. * @see https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L334
  36. * @param {string} rawName
  37. */
  38. isDefinedComponent(rawName) {
  39. if (this.names.has(rawName)) {
  40. return true
  41. }
  42. const camelName = camelize(rawName)
  43. if (this.names.has(camelName)) {
  44. return true
  45. }
  46. const pascalName = casing.capitalize(camelName)
  47. if (this.names.has(pascalName)) {
  48. return true
  49. }
  50. // Check namespace
  51. // https://github.com/vuejs/core/blob/ae4b0783d78670b6e942ae2a4e3ec6efbbffa158/packages/compiler-core/src/transforms/transformElement.ts#L305
  52. const dotIndex = rawName.indexOf('.')
  53. if (dotIndex > 0 && this.isDefinedComponent(rawName.slice(0, dotIndex))) {
  54. return true
  55. }
  56. return false
  57. }
  58. }
  59. class DefinedInOptionComponents {
  60. constructor() {
  61. /**
  62. * Component names
  63. * @type {Set<string>}
  64. */
  65. this.names = new Set()
  66. /**
  67. * Component names, transformed to kebab-case
  68. * @type {Set<string>}
  69. */
  70. this.kebabCaseNames = new Set()
  71. }
  72. /**
  73. * @param {string[]} names
  74. */
  75. addName(...names) {
  76. for (const name of names) {
  77. this.names.add(name)
  78. this.kebabCaseNames.add(casing.kebabCase(name))
  79. }
  80. }
  81. /**
  82. * @param {string} rawName
  83. */
  84. isDefinedComponent(rawName) {
  85. if (this.names.has(rawName)) {
  86. return true
  87. }
  88. const kebabCaseName = casing.kebabCase(rawName)
  89. if (
  90. this.kebabCaseNames.has(kebabCaseName) &&
  91. !casing.isPascalCase(rawName)
  92. ) {
  93. // Component registered as `foo-bar` cannot be used as `FooBar`
  94. return true
  95. }
  96. return false
  97. }
  98. }
  99. module.exports = {
  100. meta: {
  101. type: 'suggestion',
  102. docs: {
  103. description: 'disallow use of undefined components in `<template>`',
  104. categories: undefined,
  105. url: 'https://eslint.vuejs.org/rules/no-undef-components.html'
  106. },
  107. fixable: null,
  108. schema: [
  109. {
  110. type: 'object',
  111. properties: {
  112. ignorePatterns: {
  113. type: 'array'
  114. }
  115. },
  116. additionalProperties: false
  117. }
  118. ],
  119. messages: {
  120. undef: "The '<{{name}}>' component has been used, but not defined.",
  121. typeOnly:
  122. "The '<{{name}}>' component has been used, but '{{name}}' only refers to a type."
  123. }
  124. },
  125. /** @param {RuleContext} context */
  126. create(context) {
  127. const options = context.options[0] || {}
  128. /** @type {string[]} */
  129. const ignorePatterns = options.ignorePatterns || []
  130. /**
  131. * Check whether the given element name is a verify target or not.
  132. *
  133. * @param {string} rawName The element name.
  134. * @returns {boolean}
  135. */
  136. function isVerifyTargetComponent(rawName) {
  137. const kebabCaseName = casing.kebabCase(rawName)
  138. if (
  139. utils.isHtmlWellKnownElementName(rawName) ||
  140. utils.isSvgWellKnownElementName(rawName) ||
  141. utils.isMathWellKnownElementName(rawName) ||
  142. utils.isBuiltInComponentName(kebabCaseName)
  143. ) {
  144. return false
  145. }
  146. const pascalCaseName = casing.pascalCase(rawName)
  147. // Check ignored patterns
  148. if (
  149. ignorePatterns.some((pattern) => {
  150. const regExp = new RegExp(pattern)
  151. return (
  152. regExp.test(rawName) ||
  153. regExp.test(kebabCaseName) ||
  154. regExp.test(pascalCaseName)
  155. )
  156. })
  157. ) {
  158. return false
  159. }
  160. return true
  161. }
  162. /** @type { (rawName:string, reportNode: ASTNode) => void } */
  163. let verifyName
  164. /** @type {RuleListener} */
  165. let scriptVisitor = {}
  166. /** @type {TemplateListener} */
  167. const templateBodyVisitor = {
  168. VElement(node) {
  169. if (
  170. !utils.isHtmlElementNode(node) &&
  171. !utils.isSvgElementNode(node) &&
  172. !utils.isMathElementNode(node)
  173. ) {
  174. return
  175. }
  176. verifyName(node.rawName, node.startTag)
  177. },
  178. /** @param {VAttribute} node */
  179. "VAttribute[directive=false][key.name='is']"(node) {
  180. if (
  181. !node.value // `<component is />`
  182. )
  183. return
  184. const value = node.value.value.startsWith('vue:') // Usage on native elements 3.1+
  185. ? node.value.value.slice(4)
  186. : node.value.value
  187. verifyName(value, node)
  188. }
  189. }
  190. if (utils.isScriptSetup(context)) {
  191. // For <script setup>
  192. const definedInSetupComponents = new DefinedInSetupComponents()
  193. const definedInOptionComponents = new DefinedInOptionComponents()
  194. /** @type {Set<string>} */
  195. const scriptTypeOnlyNames = new Set()
  196. const globalScope = context.getSourceCode().scopeManager.globalScope
  197. if (globalScope) {
  198. for (const variable of globalScope.variables) {
  199. definedInSetupComponents.addName(variable.name)
  200. }
  201. const moduleScope = globalScope.childScopes.find(
  202. (scope) => scope.type === 'module'
  203. )
  204. for (const variable of (moduleScope && moduleScope.variables) || []) {
  205. if (
  206. // Check for type definitions. e.g. type Foo = {}
  207. (variable.isTypeVariable && !variable.isValueVariable) ||
  208. // type-only import seems to have isValueVariable set to true. So we need to check the actual Node.
  209. (variable.defs.length > 0 &&
  210. variable.defs.every((def) => {
  211. if (def.type !== 'ImportBinding') {
  212. return false
  213. }
  214. if (def.parent.importKind === 'type') {
  215. // check for `import type Foo from './xxx'`
  216. return true
  217. }
  218. if (
  219. def.node.type === 'ImportSpecifier' &&
  220. def.node.importKind === 'type'
  221. ) {
  222. // check for `import { type Foo } from './xxx'`
  223. return true
  224. }
  225. return false
  226. }))
  227. ) {
  228. scriptTypeOnlyNames.add(variable.name)
  229. } else {
  230. definedInSetupComponents.addName(variable.name)
  231. }
  232. }
  233. }
  234. // For circular references
  235. const fileName = context.getFilename()
  236. const selfComponentName = path.basename(fileName, path.extname(fileName))
  237. definedInSetupComponents.addName(selfComponentName)
  238. scriptVisitor = utils.defineVueVisitor(context, {
  239. onVueObjectEnter(node, { type }) {
  240. if (type !== 'export') return
  241. const nameProperty = utils.findProperty(node, 'name')
  242. if (nameProperty && utils.isStringLiteral(nameProperty.value)) {
  243. const name = utils.getStringLiteralValue(nameProperty.value)
  244. if (name) {
  245. definedInOptionComponents.addName(name)
  246. }
  247. }
  248. }
  249. })
  250. verifyName = (rawName, reportNode) => {
  251. if (!isVerifyTargetComponent(rawName)) {
  252. return
  253. }
  254. if (definedInSetupComponents.isDefinedComponent(rawName)) {
  255. return
  256. }
  257. if (definedInOptionComponents.isDefinedComponent(rawName)) {
  258. return
  259. }
  260. context.report({
  261. node: reportNode,
  262. messageId: scriptTypeOnlyNames.has(rawName) ? 'typeOnly' : 'undef',
  263. data: {
  264. name: rawName
  265. }
  266. })
  267. }
  268. } else {
  269. // For Options API
  270. const definedInOptionComponents = new DefinedInOptionComponents()
  271. scriptVisitor = utils.executeOnVue(context, (obj) => {
  272. definedInOptionComponents.addName(
  273. ...utils.getRegisteredComponents(obj).map(({ name }) => name)
  274. )
  275. const nameProperty = utils.findProperty(obj, 'name')
  276. if (nameProperty && utils.isStringLiteral(nameProperty.value)) {
  277. const name = utils.getStringLiteralValue(nameProperty.value)
  278. if (name) {
  279. definedInOptionComponents.addName(name)
  280. }
  281. }
  282. })
  283. verifyName = (rawName, reportNode) => {
  284. if (!isVerifyTargetComponent(rawName)) {
  285. return
  286. }
  287. if (definedInOptionComponents.isDefinedComponent(rawName)) {
  288. return
  289. }
  290. context.report({
  291. node: reportNode,
  292. messageId: 'undef',
  293. data: {
  294. name: rawName
  295. }
  296. })
  297. }
  298. /** @param {VDirective} node */
  299. templateBodyVisitor[
  300. "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=true][key.name.name='is']"
  301. ] = (node) => {
  302. if (
  303. !node.value ||
  304. node.value.type !== 'VExpressionContainer' ||
  305. !node.value.expression
  306. )
  307. return
  308. if (node.value.expression.type === 'Literal') {
  309. verifyName(`${node.value.expression.value}`, node)
  310. }
  311. }
  312. }
  313. return utils.defineTemplateBodyVisitor(
  314. context,
  315. templateBodyVisitor,
  316. scriptVisitor
  317. )
  318. }
  319. }