require-valid-default-prop.js 13 KB

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