require-expose.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. /**
  2. * @fileoverview Require `expose` in Vue components
  3. * @author Yosuke Ota <https://github.com/ota-meshi>
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const {
  10. findVariable,
  11. isOpeningBraceToken,
  12. isClosingBraceToken
  13. } = require('eslint-utils')
  14. const utils = require('../utils')
  15. const { getVueComponentDefinitionType } = require('../utils')
  16. const FIX_EXPOSE_BEFORE_OPTIONS = [
  17. 'name',
  18. 'components',
  19. 'directives',
  20. 'extends',
  21. 'mixins',
  22. 'provide',
  23. 'inject',
  24. 'inheritAttrs',
  25. 'props',
  26. 'emits'
  27. ]
  28. /**
  29. * @param {Property | SpreadElement} node
  30. * @returns {node is ObjectExpressionProperty}
  31. */
  32. function isExposeProperty(node) {
  33. return (
  34. node.type === 'Property' &&
  35. utils.getStaticPropertyName(node) === 'expose' &&
  36. !node.computed
  37. )
  38. }
  39. /**
  40. * Get the callee member node from the given CallExpression
  41. * @param {CallExpression} node CallExpression
  42. */
  43. function getCalleeMemberNode(node) {
  44. const callee = utils.skipChainExpression(node.callee)
  45. if (callee.type === 'MemberExpression') {
  46. const name = utils.getStaticPropertyName(callee)
  47. if (name) {
  48. return { name, member: callee }
  49. }
  50. }
  51. return null
  52. }
  53. module.exports = {
  54. meta: {
  55. hasSuggestions: true,
  56. type: 'suggestion',
  57. docs: {
  58. description: 'require declare public properties using `expose`',
  59. categories: undefined,
  60. url: 'https://eslint.vuejs.org/rules/require-expose.html'
  61. },
  62. fixable: null,
  63. schema: [],
  64. messages: {
  65. requireExpose:
  66. 'The public properties of the component must be explicitly declared using `expose`. If the component does not have public properties, declare it empty.',
  67. addExposeOptionForEmpty:
  68. 'Add the `expose` option to give an empty array.',
  69. addExposeOptionForAll:
  70. 'Add the `expose` option to declare all properties.'
  71. }
  72. },
  73. /** @param {RuleContext} context */
  74. create(context) {
  75. if (utils.isScriptSetup(context)) {
  76. return {}
  77. }
  78. /**
  79. * @typedef {object} SetupContext
  80. * @property {Set<Identifier>} exposeReferenceIds
  81. * @property {Set<Identifier>} contextReferenceIds
  82. */
  83. /** @type {Map<ObjectExpression, SetupContext>} */
  84. const setupContexts = new Map()
  85. /** @type {Set<ObjectExpression>} */
  86. const calledExpose = new Set()
  87. /**
  88. * @typedef {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} FunctionNode
  89. */
  90. /**
  91. * @typedef {object} ScopeStack
  92. * @property {ScopeStack | null} upper
  93. * @property {FunctionNode} functionNode
  94. * @property {boolean} returnFunction
  95. */
  96. /**
  97. * @type {ScopeStack | null}
  98. */
  99. let scopeStack = null
  100. /** @type {Map<FunctionNode, ObjectExpression>} */
  101. const setupFunctions = new Map()
  102. /** @type {Set<ObjectExpression>} */
  103. const setupRender = new Set()
  104. /**
  105. * @param {Expression} node
  106. * @returns {boolean}
  107. */
  108. function isFunction(node) {
  109. if (
  110. node.type === 'ArrowFunctionExpression' ||
  111. node.type === 'FunctionExpression'
  112. ) {
  113. return true
  114. }
  115. if (node.type === 'Identifier') {
  116. const variable = findVariable(context.getScope(), node)
  117. if (variable) {
  118. for (const def of variable.defs) {
  119. if (def.type === 'FunctionName') {
  120. return true
  121. }
  122. if (def.type === 'Variable') {
  123. if (def.node.init) {
  124. return isFunction(def.node.init)
  125. }
  126. }
  127. }
  128. }
  129. }
  130. return false
  131. }
  132. return utils.defineVueVisitor(context, {
  133. onSetupFunctionEnter(node, { node: vueNode }) {
  134. setupFunctions.set(node, vueNode)
  135. const contextParam = node.params[1]
  136. if (!contextParam) {
  137. // no arguments
  138. return
  139. }
  140. if (contextParam.type === 'RestElement') {
  141. // cannot check
  142. return
  143. }
  144. if (contextParam.type === 'ArrayPattern') {
  145. // cannot check
  146. return
  147. }
  148. /** @type {Set<Identifier>} */
  149. const contextReferenceIds = new Set()
  150. /** @type {Set<Identifier>} */
  151. const exposeReferenceIds = new Set()
  152. if (contextParam.type === 'ObjectPattern') {
  153. const exposeProperty = utils.findAssignmentProperty(
  154. contextParam,
  155. 'expose'
  156. )
  157. if (!exposeProperty) {
  158. return
  159. }
  160. const exposeParam = exposeProperty.value
  161. // `setup(props, {emit})`
  162. const variable =
  163. exposeParam.type === 'Identifier'
  164. ? findVariable(context.getScope(), exposeParam)
  165. : null
  166. if (!variable) {
  167. return
  168. }
  169. for (const reference of variable.references) {
  170. if (!reference.isRead()) {
  171. continue
  172. }
  173. exposeReferenceIds.add(reference.identifier)
  174. }
  175. } else if (contextParam.type === 'Identifier') {
  176. // `setup(props, context)`
  177. const variable = findVariable(context.getScope(), contextParam)
  178. if (!variable) {
  179. return
  180. }
  181. for (const reference of variable.references) {
  182. if (!reference.isRead()) {
  183. continue
  184. }
  185. contextReferenceIds.add(reference.identifier)
  186. }
  187. }
  188. setupContexts.set(vueNode, {
  189. contextReferenceIds,
  190. exposeReferenceIds
  191. })
  192. },
  193. CallExpression(node, { node: vueNode }) {
  194. if (calledExpose.has(vueNode)) {
  195. // already called
  196. return
  197. }
  198. // find setup context
  199. const setupContext = setupContexts.get(vueNode)
  200. if (setupContext) {
  201. const { contextReferenceIds, exposeReferenceIds } = setupContext
  202. if (
  203. node.callee.type === 'Identifier' &&
  204. exposeReferenceIds.has(node.callee)
  205. ) {
  206. // setup(props,{expose}) {expose()}
  207. calledExpose.add(vueNode)
  208. } else {
  209. const expose = getCalleeMemberNode(node)
  210. if (
  211. expose &&
  212. expose.name === 'expose' &&
  213. expose.member.object.type === 'Identifier' &&
  214. contextReferenceIds.has(expose.member.object)
  215. ) {
  216. // setup(props,context) {context.emit()}
  217. calledExpose.add(vueNode)
  218. }
  219. }
  220. }
  221. },
  222. /** @param {FunctionNode} node */
  223. ':function'(node) {
  224. scopeStack = {
  225. upper: scopeStack,
  226. functionNode: node,
  227. returnFunction: false
  228. }
  229. if (node.type === 'ArrowFunctionExpression' && node.expression) {
  230. if (isFunction(node.body)) {
  231. scopeStack.returnFunction = true
  232. }
  233. }
  234. },
  235. ReturnStatement(node) {
  236. if (!scopeStack) {
  237. return
  238. }
  239. if (!scopeStack.returnFunction && node.argument) {
  240. if (isFunction(node.argument)) {
  241. scopeStack.returnFunction = true
  242. }
  243. }
  244. },
  245. ':function:exit'(node) {
  246. if (scopeStack && scopeStack.returnFunction) {
  247. const vueNode = setupFunctions.get(node)
  248. if (vueNode) {
  249. setupRender.add(vueNode)
  250. }
  251. }
  252. scopeStack = scopeStack && scopeStack.upper
  253. },
  254. onVueObjectExit(component, { type }) {
  255. if (calledExpose.has(component)) {
  256. // `expose` function is called
  257. return
  258. }
  259. if (setupRender.has(component)) {
  260. // `setup` function is render function
  261. return
  262. }
  263. if (type === 'definition') {
  264. const defType = getVueComponentDefinitionType(component)
  265. if (defType === 'mixin') {
  266. return
  267. }
  268. }
  269. if (component.properties.some(isExposeProperty)) {
  270. // has `expose`
  271. return
  272. }
  273. context.report({
  274. node: component,
  275. messageId: 'requireExpose',
  276. suggest: buildSuggest(component, context)
  277. })
  278. }
  279. })
  280. }
  281. }
  282. /**
  283. * @param {ObjectExpression} object
  284. * @param {RuleContext} context
  285. * @returns {Rule.SuggestionReportDescriptor[]}
  286. */
  287. function buildSuggest(object, context) {
  288. const propertyNodes = object.properties.filter(utils.isProperty)
  289. const sourceCode = context.getSourceCode()
  290. const beforeOptionNode = propertyNodes.find((p) =>
  291. FIX_EXPOSE_BEFORE_OPTIONS.includes(utils.getStaticPropertyName(p) || '')
  292. )
  293. const allProps = [
  294. ...new Set(
  295. utils.iterateProperties(
  296. object,
  297. new Set(['props', 'data', 'computed', 'setup', 'methods', 'watch'])
  298. )
  299. )
  300. ]
  301. return [
  302. {
  303. messageId: 'addExposeOptionForEmpty',
  304. fix: buildFix('expose: []')
  305. },
  306. ...(allProps.length
  307. ? [
  308. {
  309. messageId: 'addExposeOptionForAll',
  310. fix: buildFix(
  311. `expose: [${allProps
  312. .map((p) => JSON.stringify(p.name))
  313. .join(', ')}]`
  314. )
  315. }
  316. ]
  317. : [])
  318. ]
  319. /**
  320. * @param {string} text
  321. */
  322. function buildFix(text) {
  323. /**
  324. * @param {RuleFixer} fixer
  325. */
  326. return (fixer) => {
  327. if (beforeOptionNode) {
  328. return fixer.insertTextAfter(beforeOptionNode, `,\n${text}`)
  329. } else if (object.properties.length) {
  330. const after = propertyNodes[0] || object.properties[0]
  331. return fixer.insertTextAfter(
  332. sourceCode.getTokenBefore(after),
  333. `\n${text},`
  334. )
  335. } else {
  336. const objectLeftBrace = /** @type {Token} */ (
  337. sourceCode.getFirstToken(object, isOpeningBraceToken)
  338. )
  339. const objectRightBrace = /** @type {Token} */ (
  340. sourceCode.getLastToken(object, isClosingBraceToken)
  341. )
  342. return fixer.insertTextAfter(
  343. objectLeftBrace,
  344. `\n${text}${
  345. objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line
  346. ? ''
  347. : '\n'
  348. }`
  349. )
  350. }
  351. }
  352. }
  353. }