require-valid-default-prop.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. /**
  2. * @fileoverview Enforces props default values to be valid.
  3. * @author Armano
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. const { capitalize } = require('../utils/casing')
  8. /**
  9. * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
  10. * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
  11. * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
  12. * @typedef {import('../utils').ComponentUnknownProp} ComponentUnknownProp
  13. * @typedef {import('../utils').VueObjectData} VueObjectData
  14. */
  15. const NATIVE_TYPES = new Set([
  16. 'String',
  17. 'Number',
  18. 'Boolean',
  19. 'Function',
  20. 'Object',
  21. 'Array',
  22. 'Symbol',
  23. 'BigInt'
  24. ])
  25. const FUNCTION_VALUE_TYPES = new Set(['Function', 'Object', 'Array'])
  26. /**
  27. * @param {ObjectExpression} obj
  28. * @param {string} name
  29. * @returns {Property | null}
  30. */
  31. function getPropertyNode(obj, name) {
  32. for (const p of obj.properties) {
  33. if (
  34. p.type === 'Property' &&
  35. !p.computed &&
  36. p.key.type === 'Identifier' &&
  37. p.key.name === name
  38. ) {
  39. return p
  40. }
  41. }
  42. return null
  43. }
  44. /**
  45. * @param {Expression} targetNode
  46. * @returns {string[]}
  47. */
  48. function getTypes(targetNode) {
  49. const node = utils.skipTSAsExpression(targetNode)
  50. if (node.type === 'Identifier') {
  51. return [node.name]
  52. } else if (node.type === 'ArrayExpression') {
  53. return node.elements
  54. .filter(
  55. /**
  56. * @param {Expression | SpreadElement | null} item
  57. * @returns {item is Identifier}
  58. */
  59. (item) => item != null && item.type === 'Identifier'
  60. )
  61. .map((item) => item.name)
  62. }
  63. return []
  64. }
  65. module.exports = {
  66. meta: {
  67. type: 'suggestion',
  68. docs: {
  69. description: 'enforce props default values to be valid',
  70. categories: ['vue3-essential', 'essential'],
  71. url: 'https://eslint.vuejs.org/rules/require-valid-default-prop.html'
  72. },
  73. fixable: null,
  74. schema: []
  75. },
  76. /** @param {RuleContext} context */
  77. create(context) {
  78. /**
  79. * @typedef {object} StandardValueType
  80. * @property {string} type
  81. * @property {false} function
  82. */
  83. /**
  84. * @typedef {object} FunctionExprValueType
  85. * @property {'Function'} type
  86. * @property {true} function
  87. * @property {true} expression
  88. * @property {Expression} functionBody
  89. * @property {string | null} returnType
  90. */
  91. /**
  92. * @typedef {object} FunctionValueType
  93. * @property {'Function'} type
  94. * @property {true} function
  95. * @property {false} expression
  96. * @property {BlockStatement} functionBody
  97. * @property {ReturnType[]} returnTypes
  98. */
  99. /**
  100. * @typedef { ComponentObjectProp & { value: ObjectExpression } } ComponentObjectDefineProp
  101. * @typedef { { type: string, node: Expression } } ReturnType
  102. */
  103. /**
  104. * @typedef {object} PropDefaultFunctionContext
  105. * @property {ComponentObjectProp | ComponentTypeProp} prop
  106. * @property {Set<string>} types
  107. * @property {FunctionValueType} default
  108. */
  109. /**
  110. * @type {Map<ObjectExpression, PropDefaultFunctionContext[]>}
  111. */
  112. const vueObjectPropsContexts = new Map()
  113. /**
  114. * @type { {node: CallExpression, props:PropDefaultFunctionContext[]}[] }
  115. */
  116. const scriptSetupPropsContexts = []
  117. /**
  118. * @typedef {object} ScopeStack
  119. * @property {ScopeStack | null} upper
  120. * @property {BlockStatement | Expression} body
  121. * @property {null | ReturnType[]} [returnTypes]
  122. */
  123. /**
  124. * @type {ScopeStack | null}
  125. */
  126. let scopeStack = null
  127. function onFunctionExit() {
  128. scopeStack = scopeStack && scopeStack.upper
  129. }
  130. /**
  131. * @param {Expression} targetNode
  132. * @returns { StandardValueType | FunctionExprValueType | FunctionValueType | null }
  133. */
  134. function getValueType(targetNode) {
  135. const node = utils.skipChainExpression(targetNode)
  136. switch (node.type) {
  137. case 'CallExpression': {
  138. // Symbol(), Number() ...
  139. if (
  140. node.callee.type === 'Identifier' &&
  141. NATIVE_TYPES.has(node.callee.name)
  142. ) {
  143. return {
  144. function: false,
  145. type: node.callee.name
  146. }
  147. }
  148. break
  149. }
  150. case 'TemplateLiteral': {
  151. // String
  152. return {
  153. function: false,
  154. type: 'String'
  155. }
  156. }
  157. case 'Literal': {
  158. // String, Boolean, Number
  159. if (node.value === null && !node.bigint) return null
  160. const type = node.bigint ? 'BigInt' : capitalize(typeof node.value)
  161. if (NATIVE_TYPES.has(type)) {
  162. return {
  163. function: false,
  164. type
  165. }
  166. }
  167. break
  168. }
  169. case 'ArrayExpression': {
  170. // Array
  171. return {
  172. function: false,
  173. type: 'Array'
  174. }
  175. }
  176. case 'ObjectExpression': {
  177. // Object
  178. return {
  179. function: false,
  180. type: 'Object'
  181. }
  182. }
  183. case 'FunctionExpression': {
  184. return {
  185. function: true,
  186. expression: false,
  187. type: 'Function',
  188. functionBody: node.body,
  189. returnTypes: []
  190. }
  191. }
  192. case 'ArrowFunctionExpression': {
  193. if (node.expression) {
  194. const valueType = getValueType(node.body)
  195. return {
  196. function: true,
  197. expression: true,
  198. type: 'Function',
  199. functionBody: node.body,
  200. returnType: valueType ? valueType.type : null
  201. }
  202. }
  203. return {
  204. function: true,
  205. expression: false,
  206. type: 'Function',
  207. functionBody: node.body,
  208. returnTypes: []
  209. }
  210. }
  211. }
  212. return null
  213. }
  214. /**
  215. * @param {*} node
  216. * @param {ComponentObjectProp | ComponentTypeProp} prop
  217. * @param {Iterable<string>} expectedTypeNames
  218. */
  219. function report(node, prop, expectedTypeNames) {
  220. const propName =
  221. prop.propName != null
  222. ? prop.propName
  223. : `[${context.getSourceCode().getText(prop.node.key)}]`
  224. context.report({
  225. node,
  226. message:
  227. "Type of the default value for '{{name}}' prop must be a {{types}}.",
  228. data: {
  229. name: propName,
  230. types: [...expectedTypeNames].join(' or ').toLowerCase()
  231. }
  232. })
  233. }
  234. /**
  235. * @param {(ComponentObjectDefineProp | ComponentTypeProp)[]} props
  236. * @param { { [key: string]: Expression | undefined } } withDefaults
  237. */
  238. function processPropDefs(props, withDefaults) {
  239. /** @type {PropDefaultFunctionContext[]} */
  240. const propContexts = []
  241. for (const prop of props) {
  242. let typeList
  243. let defExpr
  244. if (prop.type === 'object') {
  245. const type = getPropertyNode(prop.value, 'type')
  246. if (!type) continue
  247. typeList = getTypes(type.value)
  248. const def = getPropertyNode(prop.value, 'default')
  249. if (!def) continue
  250. defExpr = def.value
  251. } else {
  252. typeList = prop.types
  253. defExpr = withDefaults[prop.propName]
  254. }
  255. if (!defExpr) continue
  256. const typeNames = new Set(
  257. typeList.filter((item) => NATIVE_TYPES.has(item))
  258. )
  259. // There is no native types detected
  260. if (typeNames.size === 0) continue
  261. const defType = getValueType(defExpr)
  262. if (!defType) continue
  263. if (!defType.function) {
  264. if (
  265. typeNames.has(defType.type) &&
  266. !FUNCTION_VALUE_TYPES.has(defType.type)
  267. ) {
  268. continue
  269. }
  270. report(
  271. defExpr,
  272. prop,
  273. [...typeNames].map((type) =>
  274. FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
  275. )
  276. )
  277. } else {
  278. if (typeNames.has('Function')) {
  279. continue
  280. }
  281. if (defType.expression) {
  282. if (!defType.returnType || typeNames.has(defType.returnType)) {
  283. continue
  284. }
  285. report(defType.functionBody, prop, typeNames)
  286. } else {
  287. propContexts.push({
  288. prop,
  289. types: typeNames,
  290. default: defType
  291. })
  292. }
  293. }
  294. }
  295. return propContexts
  296. }
  297. return utils.compositingVisitors(
  298. {
  299. /**
  300. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  301. */
  302. ':function'(node) {
  303. scopeStack = {
  304. upper: scopeStack,
  305. body: node.body,
  306. returnTypes: null
  307. }
  308. },
  309. /**
  310. * @param {ReturnStatement} node
  311. */
  312. ReturnStatement(node) {
  313. if (!scopeStack) {
  314. return
  315. }
  316. if (scopeStack.returnTypes && node.argument) {
  317. const type = getValueType(node.argument)
  318. if (type) {
  319. scopeStack.returnTypes.push({
  320. type: type.type,
  321. node: node.argument
  322. })
  323. }
  324. }
  325. },
  326. ':function:exit': onFunctionExit
  327. },
  328. utils.defineVueVisitor(context, {
  329. onVueObjectEnter(obj) {
  330. /** @type {ComponentObjectDefineProp[]} */
  331. const props = utils.getComponentPropsFromOptions(obj).filter(
  332. /**
  333. * @param {ComponentObjectProp | ComponentArrayProp | ComponentUnknownProp} prop
  334. * @returns {prop is ComponentObjectDefineProp}
  335. */
  336. (prop) =>
  337. Boolean(
  338. prop.type === 'object' && prop.value.type === 'ObjectExpression'
  339. )
  340. )
  341. const propContexts = processPropDefs(props, {})
  342. vueObjectPropsContexts.set(obj, propContexts)
  343. },
  344. /**
  345. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  346. * @param {VueObjectData} data
  347. */
  348. ':function'(node, { node: vueNode }) {
  349. const data = vueObjectPropsContexts.get(vueNode)
  350. if (!data || !scopeStack) {
  351. return
  352. }
  353. for (const { default: defType } of data) {
  354. if (node.body === defType.functionBody) {
  355. scopeStack.returnTypes = defType.returnTypes
  356. }
  357. }
  358. },
  359. onVueObjectExit(obj) {
  360. const data = vueObjectPropsContexts.get(obj)
  361. if (!data) {
  362. return
  363. }
  364. for (const { prop, types: typeNames, default: defType } of data) {
  365. for (const returnType of defType.returnTypes) {
  366. if (typeNames.has(returnType.type)) continue
  367. report(returnType.node, prop, typeNames)
  368. }
  369. }
  370. }
  371. }),
  372. utils.defineScriptSetupVisitor(context, {
  373. onDefinePropsEnter(node, baseProps) {
  374. /** @type {(ComponentObjectDefineProp | ComponentTypeProp)[]} */
  375. const props = baseProps.filter(
  376. /**
  377. * @param {ComponentObjectProp | ComponentArrayProp | ComponentTypeProp | ComponentUnknownProp} prop
  378. * @returns {prop is ComponentObjectDefineProp | ComponentTypeProp}
  379. */
  380. (prop) =>
  381. Boolean(
  382. prop.type === 'type' ||
  383. (prop.type === 'object' &&
  384. prop.value.type === 'ObjectExpression')
  385. )
  386. )
  387. const defaults = utils.getWithDefaultsPropExpressions(node)
  388. const propContexts = processPropDefs(props, defaults)
  389. scriptSetupPropsContexts.push({ node, props: propContexts })
  390. },
  391. /**
  392. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  393. */
  394. ':function'(node) {
  395. const data =
  396. scriptSetupPropsContexts[scriptSetupPropsContexts.length - 1]
  397. if (!data || !scopeStack) {
  398. return
  399. }
  400. for (const { default: defType } of data.props) {
  401. if (node.body === defType.functionBody) {
  402. scopeStack.returnTypes = defType.returnTypes
  403. }
  404. }
  405. },
  406. onDefinePropsExit() {
  407. scriptSetupPropsContexts.pop()
  408. }
  409. })
  410. )
  411. }
  412. }