no-restricted-custom-event.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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 regexp = require('../utils/regexp')
  12. // ------------------------------------------------------------------------------
  13. // Helpers
  14. // ------------------------------------------------------------------------------
  15. /**
  16. * @typedef {object} ParsedOption
  17. * @property { (name: string) => boolean } test
  18. * @property {string|undefined} [message]
  19. * @property {string|undefined} [suggest]
  20. */
  21. /**
  22. * @param {string} str
  23. * @returns {(str: string) => boolean}
  24. */
  25. function buildMatcher(str) {
  26. if (regexp.isRegExp(str)) {
  27. const re = regexp.toRegExp(str)
  28. return (s) => {
  29. re.lastIndex = 0
  30. return re.test(s)
  31. }
  32. }
  33. return (s) => s === str
  34. }
  35. /**
  36. * @param {string|{event: string, message?: string, suggest?: string}} option
  37. * @returns {ParsedOption}
  38. */
  39. function parseOption(option) {
  40. if (typeof option === 'string') {
  41. const matcher = buildMatcher(option)
  42. return {
  43. test(name) {
  44. return matcher(name)
  45. }
  46. }
  47. }
  48. const parsed = parseOption(option.event)
  49. parsed.message = option.message
  50. parsed.suggest = option.suggest
  51. return parsed
  52. }
  53. /**
  54. * Get the name param node from the given CallExpression
  55. * @param {CallExpression} node CallExpression
  56. * @returns { Literal & { value: string } | null }
  57. */
  58. function getNameParamNode(node) {
  59. const nameLiteralNode = node.arguments[0]
  60. if (
  61. !nameLiteralNode ||
  62. nameLiteralNode.type !== 'Literal' ||
  63. typeof nameLiteralNode.value !== 'string'
  64. ) {
  65. // cannot check
  66. return null
  67. }
  68. return /** @type {Literal & { value: string }} */ (nameLiteralNode)
  69. }
  70. /**
  71. * Get the callee member node from the given CallExpression
  72. * @param {CallExpression} node CallExpression
  73. */
  74. function getCalleeMemberNode(node) {
  75. const callee = utils.skipChainExpression(node.callee)
  76. if (callee.type === 'MemberExpression') {
  77. const name = utils.getStaticPropertyName(callee)
  78. if (name) {
  79. return { name, member: callee }
  80. }
  81. }
  82. return null
  83. }
  84. module.exports = {
  85. meta: {
  86. hasSuggestions: true,
  87. type: 'suggestion',
  88. docs: {
  89. description: 'disallow specific custom event',
  90. categories: undefined,
  91. url: 'https://eslint.vuejs.org/rules/no-restricted-custom-event.html'
  92. },
  93. fixable: null,
  94. schema: {
  95. type: 'array',
  96. items: {
  97. oneOf: [
  98. { type: ['string'] },
  99. {
  100. type: 'object',
  101. properties: {
  102. event: { type: 'string' },
  103. message: { type: 'string', minLength: 1 },
  104. suggest: { type: 'string' }
  105. },
  106. required: ['event'],
  107. additionalProperties: false
  108. }
  109. ]
  110. },
  111. uniqueItems: true,
  112. minItems: 0
  113. },
  114. messages: {
  115. // eslint-disable-next-line eslint-plugin/report-message-format
  116. restrictedEvent: '{{message}}',
  117. instead: 'Instead, change to `{{suggest}}`.'
  118. }
  119. },
  120. /** @param {RuleContext} context */
  121. create(context) {
  122. /** @type {Map<ObjectExpression, {contextReferenceIds:Set<Identifier>,emitReferenceIds:Set<Identifier>}>} */
  123. const setupContexts = new Map()
  124. /** @type {ParsedOption[]} */
  125. const options = context.options.map(parseOption)
  126. /**
  127. * @param { Literal & { value: string } } nameLiteralNode
  128. */
  129. function verify(nameLiteralNode) {
  130. const name = nameLiteralNode.value
  131. for (const option of options) {
  132. if (option.test(name)) {
  133. const message =
  134. option.message || `Using \`${name}\` event is not allowed.`
  135. context.report({
  136. node: nameLiteralNode,
  137. messageId: 'restrictedEvent',
  138. data: { message },
  139. suggest: option.suggest
  140. ? [
  141. {
  142. fix(fixer) {
  143. const sourceCode = context.getSourceCode()
  144. return fixer.replaceText(
  145. nameLiteralNode,
  146. `${
  147. sourceCode.text[nameLiteralNode.range[0]]
  148. }${JSON.stringify(option.suggest)
  149. .slice(1, -1)
  150. .replace(/'/gu, "\\'")}${
  151. sourceCode.text[nameLiteralNode.range[1] - 1]
  152. }`
  153. )
  154. },
  155. messageId: 'instead',
  156. data: { suggest: option.suggest }
  157. }
  158. ]
  159. : []
  160. })
  161. break
  162. }
  163. }
  164. }
  165. return utils.defineTemplateBodyVisitor(
  166. context,
  167. {
  168. CallExpression(node) {
  169. const callee = node.callee
  170. const nameLiteralNode = getNameParamNode(node)
  171. if (!nameLiteralNode) {
  172. // cannot check
  173. return
  174. }
  175. if (callee.type === 'Identifier' && callee.name === '$emit') {
  176. verify(nameLiteralNode)
  177. }
  178. }
  179. },
  180. utils.compositingVisitors(
  181. utils.defineVueVisitor(context, {
  182. onSetupFunctionEnter(node, { node: vueNode }) {
  183. const contextParam = utils.skipDefaultParamValue(node.params[1])
  184. if (!contextParam) {
  185. // no arguments
  186. return
  187. }
  188. if (
  189. contextParam.type === 'RestElement' ||
  190. contextParam.type === 'ArrayPattern'
  191. ) {
  192. // cannot check
  193. return
  194. }
  195. const contextReferenceIds = new Set()
  196. const emitReferenceIds = new Set()
  197. if (contextParam.type === 'ObjectPattern') {
  198. const emitProperty = utils.findAssignmentProperty(
  199. contextParam,
  200. 'emit'
  201. )
  202. if (!emitProperty || emitProperty.value.type !== 'Identifier') {
  203. return
  204. }
  205. const emitParam = emitProperty.value
  206. // `setup(props, {emit})`
  207. const variable = findVariable(context.getScope(), emitParam)
  208. if (!variable) {
  209. return
  210. }
  211. for (const reference of variable.references) {
  212. emitReferenceIds.add(reference.identifier)
  213. }
  214. } else {
  215. // `setup(props, context)`
  216. const variable = findVariable(context.getScope(), contextParam)
  217. if (!variable) {
  218. return
  219. }
  220. for (const reference of variable.references) {
  221. contextReferenceIds.add(reference.identifier)
  222. }
  223. }
  224. setupContexts.set(vueNode, {
  225. contextReferenceIds,
  226. emitReferenceIds
  227. })
  228. },
  229. CallExpression(node, { node: vueNode }) {
  230. const nameLiteralNode = getNameParamNode(node)
  231. if (!nameLiteralNode) {
  232. // cannot check
  233. return
  234. }
  235. // verify setup context
  236. const setupContext = setupContexts.get(vueNode)
  237. if (setupContext) {
  238. const { contextReferenceIds, emitReferenceIds } = setupContext
  239. if (
  240. node.callee.type === 'Identifier' &&
  241. emitReferenceIds.has(node.callee)
  242. ) {
  243. // verify setup(props,{emit}) {emit()}
  244. verify(nameLiteralNode)
  245. } else {
  246. const emit = getCalleeMemberNode(node)
  247. if (
  248. emit &&
  249. emit.name === 'emit' &&
  250. emit.member.object.type === 'Identifier' &&
  251. contextReferenceIds.has(emit.member.object)
  252. ) {
  253. // verify setup(props,context) {context.emit()}
  254. verify(nameLiteralNode)
  255. }
  256. }
  257. }
  258. },
  259. onVueObjectExit(node) {
  260. setupContexts.delete(node)
  261. }
  262. }),
  263. {
  264. CallExpression(node) {
  265. const nameLiteralNode = getNameParamNode(node)
  266. if (!nameLiteralNode) {
  267. // cannot check
  268. return
  269. }
  270. const emit = getCalleeMemberNode(node)
  271. // verify $emit
  272. if (emit && emit.name === '$emit') {
  273. // verify this.$emit()
  274. verify(nameLiteralNode)
  275. }
  276. }
  277. }
  278. )
  279. )
  280. }
  281. }