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