no-restricted-custom-event.js 8.5 KB

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