require-explicit-emits.js 18 KB

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