require-valid-default-prop.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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').ComponentProp} ComponentProp
  10. * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
  11. * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
  12. * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
  13. * @typedef {import('../utils').ComponentInferTypeProp} ComponentInferTypeProp
  14. * @typedef {import('../utils').ComponentUnknownProp} ComponentUnknownProp
  15. * @typedef {import('../utils').VueObjectData} VueObjectData
  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. module.exports = {
  68. meta: {
  69. type: 'suggestion',
  70. docs: {
  71. description: 'enforce props default values to be valid',
  72. categories: ['vue3-essential', 'vue2-essential'],
  73. url: 'https://eslint.vuejs.org/rules/require-valid-default-prop.html'
  74. },
  75. fixable: null,
  76. schema: [],
  77. messages: {
  78. invalidType:
  79. "Type of the default value for '{{name}}' prop must be a {{types}}."
  80. }
  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 | ComponentInferTypeProp} 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. switch (node.type) {
  143. case 'CallExpression': {
  144. // Symbol(), Number() ...
  145. if (
  146. node.callee.type === 'Identifier' &&
  147. NATIVE_TYPES.has(node.callee.name)
  148. ) {
  149. return {
  150. function: false,
  151. type: node.callee.name
  152. }
  153. }
  154. break
  155. }
  156. case 'TemplateLiteral': {
  157. // String
  158. return {
  159. function: false,
  160. type: 'String'
  161. }
  162. }
  163. case 'Literal': {
  164. // String, Boolean, Number
  165. if (node.value === null && !node.bigint) return null
  166. const type = node.bigint ? 'BigInt' : capitalize(typeof node.value)
  167. if (NATIVE_TYPES.has(type)) {
  168. return {
  169. function: false,
  170. type
  171. }
  172. }
  173. break
  174. }
  175. case 'ArrayExpression': {
  176. // Array
  177. return {
  178. function: false,
  179. type: 'Array'
  180. }
  181. }
  182. case 'ObjectExpression': {
  183. // Object
  184. return {
  185. function: false,
  186. type: 'Object'
  187. }
  188. }
  189. case 'FunctionExpression': {
  190. return {
  191. function: true,
  192. expression: false,
  193. type: 'Function',
  194. functionBody: node.body,
  195. returnTypes: []
  196. }
  197. }
  198. case 'ArrowFunctionExpression': {
  199. if (node.expression) {
  200. const valueType = getValueType(node.body)
  201. return {
  202. function: true,
  203. expression: true,
  204. type: 'Function',
  205. functionBody: node.body,
  206. returnType: valueType ? valueType.type : null
  207. }
  208. }
  209. return {
  210. function: true,
  211. expression: false,
  212. type: 'Function',
  213. functionBody: node.body,
  214. returnTypes: []
  215. }
  216. }
  217. }
  218. return null
  219. }
  220. /**
  221. * @param {Expression} node
  222. * @param {ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp} prop
  223. * @param {Iterable<string>} expectedTypeNames
  224. */
  225. function report(node, prop, expectedTypeNames) {
  226. const propName =
  227. prop.propName == null
  228. ? `[${context.getSourceCode().getText(prop.node.key)}]`
  229. : prop.propName
  230. context.report({
  231. node,
  232. messageId: 'invalidType',
  233. data: {
  234. name: propName,
  235. types: [...expectedTypeNames].join(' or ').toLowerCase()
  236. }
  237. })
  238. }
  239. /**
  240. * @typedef {object} DefaultDefine
  241. * @property {Expression} expression
  242. * @property {'assignment'|'withDefaults'|'defaultProperty'} src
  243. */
  244. /**
  245. * @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
  246. * @param {(propName: string) => Iterable<DefaultDefine>} otherDefaultProvider
  247. */
  248. function processPropDefs(props, otherDefaultProvider) {
  249. /** @type {PropDefaultFunctionContext[]} */
  250. const propContexts = []
  251. for (const prop of props) {
  252. let typeList
  253. /** @type {DefaultDefine[]} */
  254. const defaultList = []
  255. if (prop.type === 'object') {
  256. if (prop.value.type === 'ObjectExpression') {
  257. const type = getPropertyNode(prop.value, 'type')
  258. if (!type) continue
  259. typeList = getTypes(type.value)
  260. const def = getPropertyNode(prop.value, 'default')
  261. if (def) {
  262. defaultList.push({
  263. src: 'defaultProperty',
  264. expression: def.value
  265. })
  266. }
  267. } else {
  268. typeList = getTypes(prop.value)
  269. }
  270. } else {
  271. typeList = prop.types
  272. }
  273. if (prop.propName != null) {
  274. defaultList.push(...otherDefaultProvider(prop.propName))
  275. }
  276. if (defaultList.length === 0) continue
  277. const typeNames = new Set(
  278. typeList.filter((item) => NATIVE_TYPES.has(item))
  279. )
  280. // There is no native types detected
  281. if (typeNames.size === 0) continue
  282. for (const defaultDef of defaultList) {
  283. const defType = getValueType(defaultDef.expression)
  284. if (!defType) continue
  285. if (defType.function) {
  286. if (typeNames.has('Function')) {
  287. continue
  288. }
  289. if (defaultDef.src === 'assignment') {
  290. // Factory functions cannot be used in default definitions with initial value assignments.
  291. report(defaultDef.expression, prop, typeNames)
  292. continue
  293. }
  294. if (defType.expression) {
  295. if (!defType.returnType || typeNames.has(defType.returnType)) {
  296. continue
  297. }
  298. report(defType.functionBody, prop, typeNames)
  299. } else {
  300. propContexts.push({
  301. prop,
  302. types: typeNames,
  303. default: defType
  304. })
  305. }
  306. } else {
  307. if (typeNames.has(defType.type)) {
  308. if (defaultDef.src === 'assignment') {
  309. continue
  310. }
  311. if (!FUNCTION_VALUE_TYPES.has(defType.type)) {
  312. // For Array and Object, defaults must be defined in the factory function.
  313. continue
  314. }
  315. }
  316. report(
  317. defaultDef.expression,
  318. prop,
  319. defaultDef.src === 'assignment'
  320. ? typeNames
  321. : [...typeNames].map((type) =>
  322. FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
  323. )
  324. )
  325. }
  326. }
  327. }
  328. return propContexts
  329. }
  330. return utils.compositingVisitors(
  331. {
  332. /**
  333. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  334. */
  335. ':function'(node) {
  336. scopeStack = {
  337. upper: scopeStack,
  338. body: node.body,
  339. returnTypes: null
  340. }
  341. },
  342. /**
  343. * @param {ReturnStatement} node
  344. */
  345. ReturnStatement(node) {
  346. if (!scopeStack) {
  347. return
  348. }
  349. if (scopeStack.returnTypes && node.argument) {
  350. const type = getValueType(node.argument)
  351. if (type) {
  352. scopeStack.returnTypes.push({
  353. type: type.type,
  354. node: node.argument
  355. })
  356. }
  357. }
  358. },
  359. ':function:exit': onFunctionExit
  360. },
  361. utils.defineVueVisitor(context, {
  362. onVueObjectEnter(obj) {
  363. /** @type {ComponentObjectDefineProp[]} */
  364. const props = utils.getComponentPropsFromOptions(obj).filter(
  365. /**
  366. * @param {ComponentObjectProp | ComponentArrayProp | ComponentUnknownProp} prop
  367. * @returns {prop is ComponentObjectDefineProp}
  368. */
  369. (prop) =>
  370. Boolean(
  371. prop.type === 'object' && prop.value.type === 'ObjectExpression'
  372. )
  373. )
  374. const propContexts = processPropDefs(props, () => [])
  375. vueObjectPropsContexts.set(obj, propContexts)
  376. },
  377. /**
  378. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  379. * @param {VueObjectData} data
  380. */
  381. ':function'(node, { node: vueNode }) {
  382. const data = vueObjectPropsContexts.get(vueNode)
  383. if (!data || !scopeStack) {
  384. return
  385. }
  386. for (const { default: defType } of data) {
  387. if (node.body === defType.functionBody) {
  388. scopeStack.returnTypes = defType.returnTypes
  389. }
  390. }
  391. },
  392. onVueObjectExit(obj) {
  393. const data = vueObjectPropsContexts.get(obj)
  394. if (!data) {
  395. return
  396. }
  397. for (const { prop, types: typeNames, default: defType } of data) {
  398. for (const returnType of defType.returnTypes) {
  399. if (typeNames.has(returnType.type)) continue
  400. report(returnType.node, prop, typeNames)
  401. }
  402. }
  403. }
  404. }),
  405. utils.defineScriptSetupVisitor(context, {
  406. onDefinePropsEnter(node, baseProps) {
  407. const props = baseProps.filter(
  408. /**
  409. * @param {ComponentProp} prop
  410. * @returns {prop is ComponentObjectProp | ComponentInferTypeProp | ComponentTypeProp}
  411. */
  412. (prop) =>
  413. Boolean(
  414. prop.type === 'type' ||
  415. prop.type === 'infer-type' ||
  416. prop.type === 'object'
  417. )
  418. )
  419. const defaultsByWithDefaults =
  420. utils.getWithDefaultsPropExpressions(node)
  421. const defaultsByAssignmentPatterns =
  422. utils.getDefaultPropExpressionsForPropsDestructure(node)
  423. const propContexts = processPropDefs(props, function* (propName) {
  424. const withDefaults = defaultsByWithDefaults[propName]
  425. if (withDefaults) {
  426. yield { src: 'withDefaults', expression: withDefaults }
  427. }
  428. const assignmentPattern = defaultsByAssignmentPatterns[propName]
  429. if (assignmentPattern) {
  430. yield {
  431. src: 'assignment',
  432. expression: assignmentPattern.expression
  433. }
  434. }
  435. })
  436. scriptSetupPropsContexts.push({ node, props: propContexts })
  437. },
  438. /**
  439. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  440. */
  441. ':function'(node) {
  442. const data =
  443. scriptSetupPropsContexts[scriptSetupPropsContexts.length - 1]
  444. if (!data || !scopeStack) {
  445. return
  446. }
  447. for (const { default: defType } of data.props) {
  448. if (node.body === defType.functionBody) {
  449. scopeStack.returnTypes = defType.returnTypes
  450. }
  451. }
  452. },
  453. onDefinePropsExit() {
  454. const data = scriptSetupPropsContexts.pop()
  455. if (!data) {
  456. return
  457. }
  458. for (const {
  459. prop,
  460. types: typeNames,
  461. default: defType
  462. } of data.props) {
  463. for (const returnType of defType.returnTypes) {
  464. if (typeNames.has(returnType.type)) continue
  465. report(returnType.node, prop, typeNames)
  466. }
  467. }
  468. }
  469. })
  470. )
  471. }
  472. }