require-explicit-emits.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. /**
  7. * @typedef {import('../utils').ComponentArrayEmit} ComponentArrayEmit
  8. * @typedef {import('../utils').ComponentObjectEmit} ComponentObjectEmit
  9. * @typedef {import('../utils').ComponentTypeEmit} ComponentTypeEmit
  10. * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
  11. * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
  12. * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
  13. * @typedef {import('../utils').VueObjectData} VueObjectData
  14. */
  15. // ------------------------------------------------------------------------------
  16. // Requirements
  17. // ------------------------------------------------------------------------------
  18. const {
  19. findVariable,
  20. isOpeningBraceToken,
  21. isClosingBraceToken,
  22. isOpeningBracketToken
  23. } = require('eslint-utils')
  24. const utils = require('../utils')
  25. const { capitalize } = require('../utils/casing')
  26. // ------------------------------------------------------------------------------
  27. // Helpers
  28. // ------------------------------------------------------------------------------
  29. const FIX_EMITS_AFTER_OPTIONS = [
  30. 'setup',
  31. 'data',
  32. 'computed',
  33. 'watch',
  34. 'methods',
  35. 'template',
  36. 'render',
  37. 'renderError',
  38. // lifecycle hooks
  39. 'beforeCreate',
  40. 'created',
  41. 'beforeMount',
  42. 'mounted',
  43. 'beforeUpdate',
  44. 'updated',
  45. 'activated',
  46. 'deactivated',
  47. 'beforeUnmount',
  48. 'unmounted',
  49. 'beforeDestroy',
  50. 'destroyed',
  51. 'renderTracked',
  52. 'renderTriggered',
  53. 'errorCaptured'
  54. ]
  55. // ------------------------------------------------------------------------------
  56. // Rule Definition
  57. // ------------------------------------------------------------------------------
  58. module.exports = {
  59. meta: {
  60. hasSuggestions: true,
  61. type: 'suggestion',
  62. docs: {
  63. description: 'require `emits` option with name triggered by `$emit()`',
  64. categories: ['vue3-strongly-recommended'],
  65. url: 'https://eslint.vuejs.org/rules/require-explicit-emits.html'
  66. },
  67. fixable: null,
  68. schema: [
  69. {
  70. type: 'object',
  71. properties: {
  72. allowProps: {
  73. type: 'boolean'
  74. }
  75. },
  76. additionalProperties: false
  77. }
  78. ],
  79. messages: {
  80. missing:
  81. 'The "{{name}}" event has been triggered but not declared on {{emitsKind}}.',
  82. addOneOption: 'Add the "{{name}}" to {{emitsKind}}.',
  83. addArrayEmitsOption:
  84. 'Add the {{emitsKind}} with array syntax and define "{{name}}" event.',
  85. addObjectEmitsOption:
  86. 'Add the {{emitsKind}} with object syntax and define "{{name}}" event.'
  87. }
  88. },
  89. /** @param {RuleContext} context */
  90. create(context) {
  91. const options = context.options[0] || {}
  92. const allowProps = !!options.allowProps
  93. /** @type {Map<ObjectExpression | Program, { contextReferenceIds: Set<Identifier>, emitReferenceIds: Set<Identifier> }>} */
  94. const setupContexts = new Map()
  95. /** @type {Map<ObjectExpression | Program, (ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]>} */
  96. const vueEmitsDeclarations = new Map()
  97. /** @type {Map<ObjectExpression | Program, (ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]>} */
  98. const vuePropsDeclarations = new Map()
  99. /**
  100. * @typedef {object} VueTemplateDefineData
  101. * @property {'export' | 'mark' | 'definition' | 'setup'} type
  102. * @property {ObjectExpression | Program} define
  103. * @property {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]} emits
  104. * @property {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} props
  105. * @property {CallExpression} [defineEmits]
  106. */
  107. /** @type {VueTemplateDefineData | null} */
  108. let vueTemplateDefineData = null
  109. /**
  110. * @param {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]} emits
  111. * @param {(ComponentArrayProp | ComponentObjectProp | ComponentTypeProp)[]} props
  112. * @param {Literal} nameLiteralNode
  113. * @param {ObjectExpression | Program} vueDefineNode
  114. */
  115. function verifyEmit(emits, props, nameLiteralNode, vueDefineNode) {
  116. const name = `${nameLiteralNode.value}`
  117. if (emits.some((e) => e.emitName === name)) {
  118. return
  119. }
  120. if (allowProps) {
  121. const key = `on${capitalize(name)}`
  122. if (props.some((e) => e.propName === key)) {
  123. return
  124. }
  125. }
  126. context.report({
  127. node: nameLiteralNode,
  128. messageId: 'missing',
  129. data: {
  130. name,
  131. emitsKind:
  132. vueDefineNode.type === 'ObjectExpression'
  133. ? '`emits` option'
  134. : '`defineEmits`'
  135. },
  136. suggest: buildSuggest(vueDefineNode, emits, nameLiteralNode, context)
  137. })
  138. }
  139. const programNode = context.getSourceCode().ast
  140. if (utils.isScriptSetup(context)) {
  141. // init
  142. vueTemplateDefineData = {
  143. type: 'setup',
  144. define: programNode,
  145. emits: [],
  146. props: []
  147. }
  148. }
  149. const callVisitor = {
  150. /**
  151. * @param {CallExpression & { arguments: [Literal, ...Expression] }} node
  152. * @param {VueObjectData} [info]
  153. */
  154. 'CallExpression[arguments.0.type=Literal]'(node, info) {
  155. const callee = utils.skipChainExpression(node.callee)
  156. const nameLiteralNode = node.arguments[0]
  157. if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
  158. // cannot check
  159. return
  160. }
  161. const vueDefineNode = info ? info.node : programNode
  162. const emitsDeclarations = vueEmitsDeclarations.get(vueDefineNode)
  163. if (!emitsDeclarations) {
  164. return
  165. }
  166. let emit
  167. if (callee.type === 'MemberExpression') {
  168. const name = utils.getStaticPropertyName(callee)
  169. if (name === 'emit' || name === '$emit') {
  170. emit = { name, member: callee }
  171. }
  172. }
  173. // verify setup context
  174. const setupContext = setupContexts.get(vueDefineNode)
  175. if (setupContext) {
  176. const { contextReferenceIds, emitReferenceIds } = setupContext
  177. if (callee.type === 'Identifier' && emitReferenceIds.has(callee)) {
  178. // verify setup(props,{emit}) {emit()}
  179. verifyEmit(
  180. emitsDeclarations,
  181. vuePropsDeclarations.get(vueDefineNode) || [],
  182. nameLiteralNode,
  183. vueDefineNode
  184. )
  185. } else if (emit && emit.name === 'emit') {
  186. const memObject = utils.skipChainExpression(emit.member.object)
  187. if (
  188. memObject.type === 'Identifier' &&
  189. contextReferenceIds.has(memObject)
  190. ) {
  191. // verify setup(props,context) {context.emit()}
  192. verifyEmit(
  193. emitsDeclarations,
  194. vuePropsDeclarations.get(vueDefineNode) || [],
  195. nameLiteralNode,
  196. vueDefineNode
  197. )
  198. }
  199. }
  200. }
  201. // verify $emit
  202. if (emit && emit.name === '$emit') {
  203. const memObject = utils.skipChainExpression(emit.member.object)
  204. if (utils.isThis(memObject, context)) {
  205. // verify this.$emit()
  206. verifyEmit(
  207. emitsDeclarations,
  208. vuePropsDeclarations.get(vueDefineNode) || [],
  209. nameLiteralNode,
  210. vueDefineNode
  211. )
  212. }
  213. }
  214. }
  215. }
  216. return utils.defineTemplateBodyVisitor(
  217. context,
  218. {
  219. /** @param { CallExpression & { argument: [Literal, ...Expression] } } node */
  220. 'CallExpression[arguments.0.type=Literal]'(node) {
  221. const callee = utils.skipChainExpression(node.callee)
  222. const nameLiteralNode = /** @type {Literal} */ (node.arguments[0])
  223. if (!nameLiteralNode || typeof nameLiteralNode.value !== 'string') {
  224. // cannot check
  225. return
  226. }
  227. if (!vueTemplateDefineData) {
  228. return
  229. }
  230. if (callee.type === 'Identifier' && callee.name === '$emit') {
  231. verifyEmit(
  232. vueTemplateDefineData.emits,
  233. vueTemplateDefineData.props,
  234. nameLiteralNode,
  235. vueTemplateDefineData.define
  236. )
  237. }
  238. }
  239. },
  240. utils.compositingVisitors(
  241. utils.defineScriptSetupVisitor(context, {
  242. onDefineEmitsEnter(node, emits) {
  243. vueEmitsDeclarations.set(programNode, emits)
  244. if (
  245. vueTemplateDefineData &&
  246. vueTemplateDefineData.type === 'setup'
  247. ) {
  248. vueTemplateDefineData.emits = emits
  249. vueTemplateDefineData.defineEmits = node
  250. }
  251. if (
  252. !node.parent ||
  253. node.parent.type !== 'VariableDeclarator' ||
  254. node.parent.init !== node
  255. ) {
  256. return
  257. }
  258. const emitParam = node.parent.id
  259. const variable =
  260. emitParam.type === 'Identifier'
  261. ? findVariable(context.getScope(), emitParam)
  262. : null
  263. if (!variable) {
  264. return
  265. }
  266. /** @type {Set<Identifier>} */
  267. const emitReferenceIds = new Set()
  268. for (const reference of variable.references) {
  269. if (!reference.isRead()) {
  270. continue
  271. }
  272. emitReferenceIds.add(reference.identifier)
  273. }
  274. setupContexts.set(programNode, {
  275. contextReferenceIds: new Set(),
  276. emitReferenceIds
  277. })
  278. },
  279. onDefinePropsEnter(_node, props) {
  280. if (allowProps) {
  281. vuePropsDeclarations.set(programNode, props)
  282. if (
  283. vueTemplateDefineData &&
  284. vueTemplateDefineData.type === 'setup'
  285. ) {
  286. vueTemplateDefineData.props = props
  287. }
  288. }
  289. },
  290. ...callVisitor
  291. }),
  292. utils.defineVueVisitor(context, {
  293. onVueObjectEnter(node) {
  294. vueEmitsDeclarations.set(node, utils.getComponentEmits(node))
  295. if (allowProps) {
  296. vuePropsDeclarations.set(node, utils.getComponentProps(node))
  297. }
  298. },
  299. onSetupFunctionEnter(node, { node: vueNode }) {
  300. const contextParam = node.params[1]
  301. if (!contextParam) {
  302. // no arguments
  303. return
  304. }
  305. if (contextParam.type === 'RestElement') {
  306. // cannot check
  307. return
  308. }
  309. if (contextParam.type === 'ArrayPattern') {
  310. // cannot check
  311. return
  312. }
  313. /** @type {Set<Identifier>} */
  314. const contextReferenceIds = new Set()
  315. /** @type {Set<Identifier>} */
  316. const emitReferenceIds = new Set()
  317. if (contextParam.type === 'ObjectPattern') {
  318. const emitProperty = utils.findAssignmentProperty(
  319. contextParam,
  320. 'emit'
  321. )
  322. if (!emitProperty) {
  323. return
  324. }
  325. const emitParam = emitProperty.value
  326. // `setup(props, {emit})`
  327. const variable =
  328. emitParam.type === 'Identifier'
  329. ? findVariable(context.getScope(), emitParam)
  330. : null
  331. if (!variable) {
  332. return
  333. }
  334. for (const reference of variable.references) {
  335. if (!reference.isRead()) {
  336. continue
  337. }
  338. emitReferenceIds.add(reference.identifier)
  339. }
  340. } else if (contextParam.type === 'Identifier') {
  341. // `setup(props, context)`
  342. const variable = findVariable(context.getScope(), contextParam)
  343. if (!variable) {
  344. return
  345. }
  346. for (const reference of variable.references) {
  347. if (!reference.isRead()) {
  348. continue
  349. }
  350. contextReferenceIds.add(reference.identifier)
  351. }
  352. }
  353. setupContexts.set(vueNode, {
  354. contextReferenceIds,
  355. emitReferenceIds
  356. })
  357. },
  358. ...callVisitor,
  359. onVueObjectExit(node, { type }) {
  360. const emits = vueEmitsDeclarations.get(node)
  361. if (
  362. !vueTemplateDefineData ||
  363. (vueTemplateDefineData.type !== 'export' &&
  364. vueTemplateDefineData.type !== 'setup')
  365. ) {
  366. if (
  367. emits &&
  368. (type === 'mark' || type === 'export' || type === 'definition')
  369. ) {
  370. vueTemplateDefineData = {
  371. type,
  372. define: node,
  373. emits,
  374. props: vuePropsDeclarations.get(node) || []
  375. }
  376. }
  377. }
  378. setupContexts.delete(node)
  379. vueEmitsDeclarations.delete(node)
  380. vuePropsDeclarations.delete(node)
  381. }
  382. })
  383. )
  384. )
  385. }
  386. }
  387. /**
  388. * @param {ObjectExpression|Program} define
  389. * @param {(ComponentArrayEmit | ComponentObjectEmit | ComponentTypeEmit)[]} emits
  390. * @param {Literal} nameNode
  391. * @param {RuleContext} context
  392. * @returns {Rule.SuggestionReportDescriptor[]}
  393. */
  394. function buildSuggest(define, emits, nameNode, context) {
  395. const emitsKind =
  396. define.type === 'ObjectExpression' ? '`emits` option' : '`defineEmits`'
  397. const certainEmits = emits.filter((e) => e.key)
  398. if (certainEmits.length) {
  399. const last = certainEmits[certainEmits.length - 1]
  400. return [
  401. {
  402. messageId: 'addOneOption',
  403. data: {
  404. name: `${nameNode.value}`,
  405. emitsKind
  406. },
  407. fix(fixer) {
  408. if (last.type === 'array') {
  409. // Array
  410. return fixer.insertTextAfter(last.node, `, '${nameNode.value}'`)
  411. } else if (last.type === 'object') {
  412. // Object
  413. return fixer.insertTextAfter(
  414. last.node,
  415. `, '${nameNode.value}': null`
  416. )
  417. } else {
  418. // type
  419. // The argument is unknown and cannot be suggested.
  420. return null
  421. }
  422. }
  423. }
  424. ]
  425. }
  426. if (define.type !== 'ObjectExpression') {
  427. // We don't know where to put defineEmits.
  428. return []
  429. }
  430. const object = define
  431. const propertyNodes = object.properties.filter(utils.isProperty)
  432. const emitsOption = propertyNodes.find(
  433. (p) => utils.getStaticPropertyName(p) === 'emits'
  434. )
  435. if (emitsOption) {
  436. const sourceCode = context.getSourceCode()
  437. const emitsOptionValue = emitsOption.value
  438. if (emitsOptionValue.type === 'ArrayExpression') {
  439. const leftBracket = /** @type {Token} */ (
  440. sourceCode.getFirstToken(emitsOptionValue, isOpeningBracketToken)
  441. )
  442. return [
  443. {
  444. messageId: 'addOneOption',
  445. data: { name: `${nameNode.value}`, emitsKind },
  446. fix(fixer) {
  447. return fixer.insertTextAfter(
  448. leftBracket,
  449. `'${nameNode.value}'${
  450. emitsOptionValue.elements.length ? ',' : ''
  451. }`
  452. )
  453. }
  454. }
  455. ]
  456. } else if (emitsOptionValue.type === 'ObjectExpression') {
  457. const leftBrace = /** @type {Token} */ (
  458. sourceCode.getFirstToken(emitsOptionValue, isOpeningBraceToken)
  459. )
  460. return [
  461. {
  462. messageId: 'addOneOption',
  463. data: { name: `${nameNode.value}`, emitsKind },
  464. fix(fixer) {
  465. return fixer.insertTextAfter(
  466. leftBrace,
  467. `'${nameNode.value}': null${
  468. emitsOptionValue.properties.length ? ',' : ''
  469. }`
  470. )
  471. }
  472. }
  473. ]
  474. }
  475. return []
  476. }
  477. const sourceCode = context.getSourceCode()
  478. const afterOptionNode = propertyNodes.find((p) =>
  479. FIX_EMITS_AFTER_OPTIONS.includes(utils.getStaticPropertyName(p) || '')
  480. )
  481. return [
  482. {
  483. messageId: 'addArrayEmitsOption',
  484. data: { name: `${nameNode.value}`, emitsKind },
  485. fix(fixer) {
  486. if (afterOptionNode) {
  487. return fixer.insertTextAfter(
  488. sourceCode.getTokenBefore(afterOptionNode),
  489. `\nemits: ['${nameNode.value}'],`
  490. )
  491. } else if (object.properties.length) {
  492. const before =
  493. propertyNodes[propertyNodes.length - 1] ||
  494. object.properties[object.properties.length - 1]
  495. return fixer.insertTextAfter(
  496. before,
  497. `,\nemits: ['${nameNode.value}']`
  498. )
  499. } else {
  500. const objectLeftBrace = /** @type {Token} */ (
  501. sourceCode.getFirstToken(object, isOpeningBraceToken)
  502. )
  503. const objectRightBrace = /** @type {Token} */ (
  504. sourceCode.getLastToken(object, isClosingBraceToken)
  505. )
  506. return fixer.insertTextAfter(
  507. objectLeftBrace,
  508. `\nemits: ['${nameNode.value}']${
  509. objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line
  510. ? ''
  511. : '\n'
  512. }`
  513. )
  514. }
  515. }
  516. },
  517. {
  518. messageId: 'addObjectEmitsOption',
  519. data: { name: `${nameNode.value}`, emitsKind },
  520. fix(fixer) {
  521. if (afterOptionNode) {
  522. return fixer.insertTextAfter(
  523. sourceCode.getTokenBefore(afterOptionNode),
  524. `\nemits: {'${nameNode.value}': null},`
  525. )
  526. } else if (object.properties.length) {
  527. const before =
  528. propertyNodes[propertyNodes.length - 1] ||
  529. object.properties[object.properties.length - 1]
  530. return fixer.insertTextAfter(
  531. before,
  532. `,\nemits: {'${nameNode.value}': null}`
  533. )
  534. } else {
  535. const objectLeftBrace = /** @type {Token} */ (
  536. sourceCode.getFirstToken(object, isOpeningBraceToken)
  537. )
  538. const objectRightBrace = /** @type {Token} */ (
  539. sourceCode.getLastToken(object, isClosingBraceToken)
  540. )
  541. return fixer.insertTextAfter(
  542. objectLeftBrace,
  543. `\nemits: {'${nameNode.value}': null}${
  544. objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line
  545. ? ''
  546. : '\n'
  547. }`
  548. )
  549. }
  550. }
  551. }
  552. ]
  553. }