custom-event-name-casing.js 7.8 KB

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