custom-event-name-casing.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const { findVariable } = require('@eslint-community/eslint-utils')
  7. const utils = require('../utils')
  8. const casing = require('../utils/casing')
  9. const { toRegExp } = require('../utils/regexp')
  10. /**
  11. * @typedef {import('../utils').VueObjectData} VueObjectData
  12. */
  13. const ALLOWED_CASE_OPTIONS = ['kebab-case', 'camelCase']
  14. const DEFAULT_CASE = 'camelCase'
  15. /**
  16. * @typedef {object} NameWithLoc
  17. * @property {string} name
  18. * @property {SourceLocation} loc
  19. */
  20. /**
  21. * Get the name param node from the given CallExpression
  22. * @param {CallExpression} node CallExpression
  23. * @returns { NameWithLoc | null }
  24. */
  25. function getNameParamNode(node) {
  26. const nameLiteralNode = node.arguments[0]
  27. if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) {
  28. const name = utils.getStringLiteralValue(nameLiteralNode)
  29. if (name != null) {
  30. return { name, loc: nameLiteralNode.loc }
  31. }
  32. }
  33. // cannot check
  34. return null
  35. }
  36. /**
  37. * Get the callee member node from the given CallExpression
  38. * @param {CallExpression} node CallExpression
  39. */
  40. function getCalleeMemberNode(node) {
  41. const callee = utils.skipChainExpression(node.callee)
  42. if (callee.type === 'MemberExpression') {
  43. const name = utils.getStaticPropertyName(callee)
  44. if (name) {
  45. return { name, member: callee }
  46. }
  47. }
  48. return null
  49. }
  50. const OBJECT_OPTION_SCHEMA = {
  51. type: 'object',
  52. properties: {
  53. ignores: {
  54. type: 'array',
  55. items: { type: 'string' },
  56. uniqueItems: true,
  57. additionalItems: false
  58. }
  59. },
  60. additionalProperties: false
  61. }
  62. module.exports = {
  63. meta: {
  64. type: 'suggestion',
  65. docs: {
  66. description: 'enforce specific casing for custom event name',
  67. categories: undefined,
  68. url: 'https://eslint.vuejs.org/rules/custom-event-name-casing.html'
  69. },
  70. fixable: null,
  71. schema: {
  72. anyOf: [
  73. {
  74. type: 'array',
  75. items: [
  76. {
  77. enum: ALLOWED_CASE_OPTIONS
  78. },
  79. OBJECT_OPTION_SCHEMA
  80. ]
  81. },
  82. // For backward compatibility
  83. {
  84. type: 'array',
  85. items: [OBJECT_OPTION_SCHEMA]
  86. }
  87. ]
  88. },
  89. messages: {
  90. unexpected: "Custom event name '{{name}}' must be {{caseType}}."
  91. }
  92. },
  93. /** @param {RuleContext} context */
  94. create(context) {
  95. /** @type {Map<ObjectExpression|Program, {contextReferenceIds:Set<Identifier>,emitReferenceIds:Set<Identifier>}>} */
  96. const setupContexts = new Map()
  97. let emitParamName = ''
  98. const options =
  99. context.options.length === 1 && typeof context.options[0] !== 'string'
  100. ? // For backward compatibility
  101. [undefined, context.options[0]]
  102. : context.options
  103. const caseType = options[0] || DEFAULT_CASE
  104. const objectOption = options[1] || {}
  105. const caseChecker = casing.getChecker(caseType)
  106. /** @type {RegExp[]} */
  107. const ignores = (objectOption.ignores || []).map(toRegExp)
  108. /**
  109. * Check whether the given event name is valid.
  110. * @param {string} name The name to check.
  111. * @returns {boolean} `true` if the given event name is valid.
  112. */
  113. function isValidEventName(name) {
  114. return caseChecker(name) || name.startsWith('update:')
  115. }
  116. /**
  117. * @param { NameWithLoc } nameWithLoc
  118. */
  119. function verify(nameWithLoc) {
  120. const name = nameWithLoc.name
  121. if (isValidEventName(name) || ignores.some((re) => re.test(name))) {
  122. return
  123. }
  124. context.report({
  125. loc: nameWithLoc.loc,
  126. messageId: 'unexpected',
  127. data: {
  128. name,
  129. caseType
  130. }
  131. })
  132. }
  133. const programNode = context.getSourceCode().ast
  134. const callVisitor = {
  135. /**
  136. * @param {CallExpression} node
  137. * @param {VueObjectData} [info]
  138. */
  139. CallExpression(node, info) {
  140. const nameWithLoc = getNameParamNode(node)
  141. if (!nameWithLoc) {
  142. // cannot check
  143. return
  144. }
  145. // verify setup context
  146. const setupContext = setupContexts.get(info ? info.node : programNode)
  147. if (setupContext) {
  148. const { contextReferenceIds, emitReferenceIds } = setupContext
  149. if (
  150. node.callee.type === 'Identifier' &&
  151. emitReferenceIds.has(node.callee)
  152. ) {
  153. // verify setup(props,{emit}) {emit()}
  154. verify(nameWithLoc)
  155. } else {
  156. const emit = getCalleeMemberNode(node)
  157. if (
  158. emit &&
  159. emit.name === 'emit' &&
  160. emit.member.object.type === 'Identifier' &&
  161. contextReferenceIds.has(emit.member.object)
  162. ) {
  163. // verify setup(props,context) {context.emit()}
  164. verify(nameWithLoc)
  165. }
  166. }
  167. }
  168. }
  169. }
  170. return utils.defineTemplateBodyVisitor(
  171. context,
  172. {
  173. CallExpression(node) {
  174. const callee = node.callee
  175. const nameWithLoc = getNameParamNode(node)
  176. if (!nameWithLoc) {
  177. // cannot check
  178. return
  179. }
  180. if (
  181. callee.type === 'Identifier' &&
  182. (callee.name === '$emit' || callee.name === emitParamName)
  183. ) {
  184. verify(nameWithLoc)
  185. }
  186. }
  187. },
  188. utils.compositingVisitors(
  189. utils.defineScriptSetupVisitor(context, {
  190. onDefineEmitsEnter(node) {
  191. if (
  192. !node.parent ||
  193. node.parent.type !== 'VariableDeclarator' ||
  194. node.parent.init !== node
  195. ) {
  196. return
  197. }
  198. const emitParam = node.parent.id
  199. if (emitParam.type !== 'Identifier') {
  200. return
  201. }
  202. emitParamName = emitParam.name
  203. // const emit = defineEmits()
  204. const variable = findVariable(
  205. utils.getScope(context, emitParam),
  206. emitParam
  207. )
  208. if (!variable) {
  209. return
  210. }
  211. const emitReferenceIds = new Set()
  212. for (const reference of variable.references) {
  213. emitReferenceIds.add(reference.identifier)
  214. }
  215. setupContexts.set(programNode, {
  216. contextReferenceIds: new Set(),
  217. emitReferenceIds
  218. })
  219. },
  220. ...callVisitor
  221. }),
  222. utils.defineVueVisitor(context, {
  223. onSetupFunctionEnter(node, { node: vueNode }) {
  224. const contextParam = utils.skipDefaultParamValue(node.params[1])
  225. if (!contextParam) {
  226. // no arguments
  227. return
  228. }
  229. if (
  230. contextParam.type === 'RestElement' ||
  231. contextParam.type === 'ArrayPattern'
  232. ) {
  233. // cannot check
  234. return
  235. }
  236. const contextReferenceIds = new Set()
  237. const emitReferenceIds = new Set()
  238. if (contextParam.type === 'ObjectPattern') {
  239. const emitProperty = utils.findAssignmentProperty(
  240. contextParam,
  241. 'emit'
  242. )
  243. if (!emitProperty || emitProperty.value.type !== 'Identifier') {
  244. return
  245. }
  246. const emitParam = emitProperty.value
  247. // `setup(props, {emit})`
  248. const variable = findVariable(
  249. utils.getScope(context, emitParam),
  250. emitParam
  251. )
  252. if (!variable) {
  253. return
  254. }
  255. for (const reference of variable.references) {
  256. emitReferenceIds.add(reference.identifier)
  257. }
  258. } else {
  259. // `setup(props, context)`
  260. const variable = findVariable(
  261. utils.getScope(context, contextParam),
  262. contextParam
  263. )
  264. if (!variable) {
  265. return
  266. }
  267. for (const reference of variable.references) {
  268. contextReferenceIds.add(reference.identifier)
  269. }
  270. }
  271. setupContexts.set(vueNode, {
  272. contextReferenceIds,
  273. emitReferenceIds
  274. })
  275. },
  276. ...callVisitor,
  277. onVueObjectExit(node) {
  278. setupContexts.delete(node)
  279. }
  280. }),
  281. {
  282. CallExpression(node) {
  283. const nameLiteralNode = getNameParamNode(node)
  284. if (!nameLiteralNode) {
  285. // cannot check
  286. return
  287. }
  288. const emit = getCalleeMemberNode(node)
  289. // verify $emit
  290. if (emit && emit.name === '$emit') {
  291. // verify this.$emit()
  292. verify(nameLiteralNode)
  293. }
  294. }
  295. }
  296. )
  297. )
  298. }
  299. }