no-unused-emit-declarations.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /**
  2. * @author ItMaga
  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. /**
  9. * @typedef {import('../utils').ComponentEmit} ComponentEmit
  10. * @typedef {import('../utils').VueObjectData} VueObjectData
  11. */
  12. /**
  13. * @typedef {object} SetupContext
  14. * @property {Set<Identifier>} contextReferenceIds
  15. * @property {Set<Identifier>} emitReferenceIds
  16. */
  17. /**
  18. * @typedef {object} NameWithLoc
  19. * @property {string} name
  20. * @property {SourceLocation} loc
  21. * @property {Range} range
  22. */
  23. /**
  24. * Get the name param node from the given CallExpression
  25. * @param {CallExpression} node CallExpression
  26. * @returns {NameWithLoc | null}
  27. */
  28. function getNameParamNode(node) {
  29. const nameLiteralNode = node.arguments[0]
  30. if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) {
  31. const name = utils.getStringLiteralValue(nameLiteralNode)
  32. if (name != null) {
  33. return { name, loc: nameLiteralNode.loc, range: nameLiteralNode.range }
  34. }
  35. }
  36. return null
  37. }
  38. /**
  39. * Check if the given node is a reference of setup context
  40. * @param {Expression | Super | SpreadElement} value
  41. * @param {SetupContext} setupContext
  42. * @returns {boolean}
  43. * */
  44. function hasReferenceId(value, setupContext) {
  45. const { contextReferenceIds, emitReferenceIds } = setupContext
  46. return (
  47. value.type === 'Identifier' &&
  48. (emitReferenceIds.has(value) || contextReferenceIds.has(value))
  49. )
  50. }
  51. module.exports = {
  52. meta: {
  53. type: 'suggestion',
  54. docs: {
  55. description: 'disallow unused emit declarations',
  56. categories: undefined,
  57. url: 'https://eslint.vuejs.org/rules/no-unused-emit-declarations.html'
  58. },
  59. fixable: null,
  60. schema: [],
  61. messages: {
  62. unused: '`{{name}}` is defined as emit but never used.'
  63. }
  64. },
  65. /** @param {RuleContext} context */
  66. create(context) {
  67. /** @type {Map<string, ComponentEmit>} */
  68. const emitDeclarations = new Map()
  69. /** @type {Map<string, Expression>} */
  70. const emitCalls = new Map()
  71. /** @type {Map<ObjectExpression | Program, SetupContext>} */
  72. const setupContexts = new Map()
  73. const programNode = context.getSourceCode().ast
  74. let emitParamName = ''
  75. /**
  76. * @param {CallExpression} node
  77. * */
  78. function addEmitCall(node) {
  79. const nameParamNode = getNameParamNode(node)
  80. if (nameParamNode) {
  81. emitCalls.set(nameParamNode.name, node)
  82. }
  83. }
  84. function clearEmits() {
  85. emitCalls.clear()
  86. emitDeclarations.clear()
  87. }
  88. /**
  89. * @param {Expression | SpreadElement} expression
  90. * @param {SetupContext} setupContext
  91. * @returns {boolean}
  92. * */
  93. function checkExpressionReference(expression, setupContext) {
  94. if (expression.type === 'MemberExpression') {
  95. const memObject = utils.skipChainExpression(expression.object)
  96. if (hasReferenceId(memObject, setupContext)) {
  97. clearEmits()
  98. return true
  99. }
  100. }
  101. if (hasReferenceId(expression, setupContext)) {
  102. clearEmits()
  103. return true
  104. }
  105. return false
  106. }
  107. /**
  108. *
  109. * @param {Array<Expression | SpreadElement>} args
  110. * @param {SetupContext} setupContext
  111. * @returns {boolean}
  112. */
  113. function verifyArgumentsReferences(args, setupContext) {
  114. for (const argument of args) {
  115. if (argument.type === 'ObjectExpression') {
  116. for (const property of argument.properties) {
  117. if (
  118. property.type === 'Property' &&
  119. checkExpressionReference(property.value, setupContext)
  120. ) {
  121. return true
  122. }
  123. }
  124. }
  125. if (argument.type === 'ArrayExpression') {
  126. for (const element of argument.elements) {
  127. if (!element) {
  128. continue
  129. }
  130. if (checkExpressionReference(element, setupContext)) {
  131. return true
  132. }
  133. }
  134. }
  135. if (checkExpressionReference(argument, setupContext)) {
  136. return true
  137. }
  138. }
  139. return false
  140. }
  141. /**
  142. * @param {Expression | Super} callee
  143. * @param {Set<Identifier>} referenceIds
  144. * @param {CallExpression} node
  145. * */
  146. function addEmitCallByReference(callee, referenceIds, node) {
  147. if (callee.type === 'Identifier' && referenceIds.has(callee)) {
  148. addEmitCall(node)
  149. }
  150. }
  151. const callVisitor = {
  152. /**
  153. * @param {CallExpression} node
  154. * @param {VueObjectData} [info]
  155. */
  156. CallExpression(node, info) {
  157. const callee = utils.skipChainExpression(node.callee)
  158. let emit = null
  159. if (callee.type === 'MemberExpression') {
  160. const name = utils.getStaticPropertyName(callee)
  161. if (name === 'emit' || name === '$emit') {
  162. emit = { name, member: callee }
  163. }
  164. }
  165. const vueDefineNode = info ? info.node : programNode
  166. const setupContext = setupContexts.get(vueDefineNode)
  167. if (setupContext) {
  168. if (
  169. callee.parent.type === 'CallExpression' &&
  170. callee.parent.arguments &&
  171. verifyArgumentsReferences(callee.parent.arguments, setupContext)
  172. ) {
  173. // skip if the emit passed as argument
  174. return
  175. }
  176. const { contextReferenceIds, emitReferenceIds } = setupContext
  177. // verify setup(props,{emit}) {emit()}
  178. addEmitCallByReference(callee, emitReferenceIds, node)
  179. if (emit && emit.name === 'emit') {
  180. const memObject = utils.skipChainExpression(emit.member.object)
  181. // verify setup(props,context) {context.emit()}
  182. addEmitCallByReference(memObject, contextReferenceIds, node)
  183. }
  184. }
  185. if (emit && emit.name === '$emit') {
  186. const memObject = utils.skipChainExpression(emit.member.object)
  187. // verify this.$emit()
  188. if (utils.isThis(memObject, context)) {
  189. addEmitCall(node)
  190. }
  191. }
  192. // verify $emit() and defineEmits variable in template
  193. if (
  194. callee.type === 'Identifier' &&
  195. (callee.name === '$emit' || callee.name === emitParamName)
  196. ) {
  197. addEmitCall(node)
  198. }
  199. }
  200. }
  201. return utils.compositingVisitors(
  202. utils.defineTemplateBodyVisitor(context, callVisitor),
  203. utils.defineVueVisitor(context, {
  204. ...callVisitor,
  205. onVueObjectEnter(node) {
  206. for (const emit of utils.getComponentEmitsFromOptions(node)) {
  207. if (emit.emitName) {
  208. emitDeclarations.set(emit.emitName, emit)
  209. }
  210. }
  211. },
  212. onSetupFunctionEnter(node, { node: vueNode }) {
  213. const contextParam = node.params[1]
  214. if (
  215. !contextParam ||
  216. contextParam.type === 'RestElement' ||
  217. contextParam.type === 'ArrayPattern'
  218. ) {
  219. // no arguments or cannot check
  220. return
  221. }
  222. /** @type {Set<Identifier>} */
  223. const contextReferenceIds = new Set()
  224. /** @type {Set<Identifier>} */
  225. const emitReferenceIds = new Set()
  226. if (contextParam.type === 'ObjectPattern') {
  227. const emitProperty = utils.findAssignmentProperty(
  228. contextParam,
  229. 'emit'
  230. )
  231. if (!emitProperty) {
  232. return
  233. }
  234. const emitParam = emitProperty.value
  235. // `setup(props, {emit})`
  236. const variable =
  237. emitParam.type === 'Identifier'
  238. ? findVariable(utils.getScope(context, emitParam), emitParam)
  239. : null
  240. if (!variable) {
  241. return
  242. }
  243. for (const reference of variable.references) {
  244. emitReferenceIds.add(reference.identifier)
  245. }
  246. } else if (contextParam.type === 'Identifier') {
  247. // `setup(props, context)`
  248. const variable = findVariable(
  249. utils.getScope(context, contextParam),
  250. contextParam
  251. )
  252. for (const reference of variable.references) {
  253. contextReferenceIds.add(reference.identifier)
  254. }
  255. }
  256. setupContexts.set(vueNode, {
  257. contextReferenceIds,
  258. emitReferenceIds
  259. })
  260. }
  261. }),
  262. utils.defineScriptSetupVisitor(context, {
  263. onDefineEmitsEnter(node, emits) {
  264. for (const emit of emits) {
  265. if (emit.emitName) {
  266. emitDeclarations.set(emit.emitName, emit)
  267. }
  268. }
  269. if (
  270. !node.parent ||
  271. node.parent.type !== 'VariableDeclarator' ||
  272. node.parent.init !== node
  273. ) {
  274. return
  275. }
  276. const emitParam = node.parent.id
  277. if (emitParam.type !== 'Identifier') {
  278. return
  279. }
  280. emitParamName = emitParam.name
  281. const variable = findVariable(
  282. utils.getScope(context, emitParam),
  283. emitParam
  284. )
  285. if (!variable) {
  286. return
  287. }
  288. /** @type {Set<Identifier>} */
  289. const emitReferenceIds = new Set()
  290. for (const reference of variable.references) {
  291. emitReferenceIds.add(reference.identifier)
  292. }
  293. setupContexts.set(programNode, {
  294. contextReferenceIds: new Set(),
  295. emitReferenceIds
  296. })
  297. },
  298. onDefineModelEnter(node, model) {
  299. if (
  300. node.parent &&
  301. node.parent.type === 'VariableDeclarator' &&
  302. node.parent.init === node
  303. ) {
  304. // If the return value of defineModel() is stored in a variable, we can mark the 'update:modelName' event as used if that that variable is used.
  305. // If that variable is unused, it will already be reported by `no-unused-var` rule.
  306. emitCalls.set(`update:${model.name.modelName}`, node)
  307. }
  308. },
  309. ...callVisitor
  310. }),
  311. {
  312. 'Program:exit'() {
  313. for (const [name, emit] of emitDeclarations) {
  314. if (!emitCalls.has(name) && emit.node) {
  315. context.report({
  316. node: emit.node,
  317. loc: emit.node.loc,
  318. messageId: 'unused',
  319. data: { name }
  320. })
  321. }
  322. }
  323. }
  324. }
  325. )
  326. }
  327. }