require-valid-default-prop.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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').VueObjectData} VueObjectData
  12. */
  13. // ----------------------------------------------------------------------
  14. // Helpers
  15. // ----------------------------------------------------------------------
  16. const NATIVE_TYPES = new Set([
  17. 'String',
  18. 'Number',
  19. 'Boolean',
  20. 'Function',
  21. 'Object',
  22. 'Array',
  23. 'Symbol',
  24. 'BigInt'
  25. ])
  26. const FUNCTION_VALUE_TYPES = new Set(['Function', 'Object', 'Array'])
  27. /**
  28. * @param {ObjectExpression} obj
  29. * @param {string} name
  30. * @returns {Property | null}
  31. */
  32. function getPropertyNode(obj, name) {
  33. for (const p of obj.properties) {
  34. if (
  35. p.type === 'Property' &&
  36. !p.computed &&
  37. p.key.type === 'Identifier' &&
  38. p.key.name === name
  39. ) {
  40. return p
  41. }
  42. }
  43. return null
  44. }
  45. /**
  46. * @param {Expression} node
  47. * @returns {string[]}
  48. */
  49. function getTypes(node) {
  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. // ------------------------------------------------------------------------------
  66. // Rule Definition
  67. // ------------------------------------------------------------------------------
  68. module.exports = {
  69. meta: {
  70. type: 'suggestion',
  71. docs: {
  72. description: 'enforce props default values to be valid',
  73. categories: ['vue3-essential', 'essential'],
  74. url: 'https://eslint.vuejs.org/rules/require-valid-default-prop.html'
  75. },
  76. fixable: null,
  77. schema: []
  78. },
  79. /** @param {RuleContext} context */
  80. create(context) {
  81. /**
  82. * @typedef { { type: string, function: false } } StandardValueType
  83. * @typedef { { type: 'Function', function: true, expression: true, functionBody: Expression, returnType: string | null } } FunctionExprValueType
  84. * @typedef { { type: 'Function', function: true, expression: false, functionBody: BlockStatement, returnTypes: ReturnType[] } } FunctionValueType
  85. * @typedef { ComponentObjectProp & { value: ObjectExpression } } ComponentObjectDefineProp
  86. * @typedef { { prop: ComponentObjectDefineProp, type: Set<string>, default: FunctionValueType } } PropDefaultFunctionContext
  87. * @typedef { { type: string, node: Expression } } ReturnType
  88. */
  89. /**
  90. * @type {Map<ObjectExpression, PropDefaultFunctionContext[]>}
  91. */
  92. const vueObjectPropsContexts = new Map()
  93. /**
  94. * @typedef {object} ScopeStack
  95. * @property {ScopeStack | null} upper
  96. * @property {BlockStatement | Expression} body
  97. * @property {null | ReturnType[]} [returnTypes]
  98. */
  99. /**
  100. * @type {ScopeStack | null}
  101. */
  102. let scopeStack = null
  103. function onFunctionExit() {
  104. scopeStack = scopeStack && scopeStack.upper
  105. }
  106. /**
  107. * @param {Expression} targetNode
  108. * @returns { StandardValueType | FunctionExprValueType | FunctionValueType | null }
  109. */
  110. function getValueType(targetNode) {
  111. const node = utils.skipChainExpression(targetNode)
  112. if (node.type === 'CallExpression') {
  113. // Symbol(), Number() ...
  114. if (
  115. node.callee.type === 'Identifier' &&
  116. NATIVE_TYPES.has(node.callee.name)
  117. ) {
  118. return {
  119. function: false,
  120. type: node.callee.name
  121. }
  122. }
  123. } else if (node.type === 'TemplateLiteral') {
  124. // String
  125. return {
  126. function: false,
  127. type: 'String'
  128. }
  129. } else if (node.type === 'Literal') {
  130. // String, Boolean, Number
  131. if (node.value === null && !node.bigint) return null
  132. const type = node.bigint ? 'BigInt' : capitalize(typeof node.value)
  133. if (NATIVE_TYPES.has(type)) {
  134. return {
  135. function: false,
  136. type
  137. }
  138. }
  139. } else if (node.type === 'ArrayExpression') {
  140. // Array
  141. return {
  142. function: false,
  143. type: 'Array'
  144. }
  145. } else if (node.type === 'ObjectExpression') {
  146. // Object
  147. return {
  148. function: false,
  149. type: 'Object'
  150. }
  151. } else if (node.type === 'FunctionExpression') {
  152. return {
  153. function: true,
  154. expression: false,
  155. type: 'Function',
  156. functionBody: node.body,
  157. returnTypes: []
  158. }
  159. } else if (node.type === 'ArrowFunctionExpression') {
  160. if (node.expression) {
  161. const valueType = getValueType(node.body)
  162. return {
  163. function: true,
  164. expression: true,
  165. type: 'Function',
  166. functionBody: node.body,
  167. returnType: valueType ? valueType.type : null
  168. }
  169. } else {
  170. return {
  171. function: true,
  172. expression: false,
  173. type: 'Function',
  174. functionBody: node.body,
  175. returnTypes: []
  176. }
  177. }
  178. }
  179. return null
  180. }
  181. /**
  182. * @param {*} node
  183. * @param {ComponentObjectProp} prop
  184. * @param {Iterable<string>} expectedTypeNames
  185. */
  186. function report(node, prop, expectedTypeNames) {
  187. const propName =
  188. prop.propName != null
  189. ? prop.propName
  190. : `[${context.getSourceCode().getText(prop.node.key)}]`
  191. context.report({
  192. node,
  193. message:
  194. "Type of the default value for '{{name}}' prop must be a {{types}}.",
  195. data: {
  196. name: propName,
  197. types: Array.from(expectedTypeNames).join(' or ').toLowerCase()
  198. }
  199. })
  200. }
  201. // ----------------------------------------------------------------------
  202. // Public
  203. // ----------------------------------------------------------------------
  204. return utils.defineVueVisitor(context, {
  205. onVueObjectEnter(obj) {
  206. /** @type {ComponentObjectDefineProp[]} */
  207. const props = utils.getComponentProps(obj).filter(
  208. /**
  209. * @param {ComponentObjectProp | ComponentArrayProp} prop
  210. * @returns {prop is ComponentObjectDefineProp}
  211. */
  212. (prop) =>
  213. Boolean(prop.value && prop.value.type === 'ObjectExpression')
  214. )
  215. /** @type {PropDefaultFunctionContext[]} */
  216. const propContexts = []
  217. for (const prop of props) {
  218. const type = getPropertyNode(prop.value, 'type')
  219. if (!type) continue
  220. const typeNames = new Set(
  221. getTypes(type.value).filter((item) => NATIVE_TYPES.has(item))
  222. )
  223. // There is no native types detected
  224. if (typeNames.size === 0) continue
  225. const def = getPropertyNode(prop.value, 'default')
  226. if (!def) continue
  227. const defType = getValueType(def.value)
  228. if (!defType) continue
  229. if (!defType.function) {
  230. if (typeNames.has(defType.type)) {
  231. if (!FUNCTION_VALUE_TYPES.has(defType.type)) {
  232. continue
  233. }
  234. }
  235. report(
  236. def.value,
  237. prop,
  238. Array.from(typeNames).map((type) =>
  239. FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
  240. )
  241. )
  242. } else {
  243. if (typeNames.has('Function')) {
  244. continue
  245. }
  246. if (defType.expression) {
  247. if (!defType.returnType || typeNames.has(defType.returnType)) {
  248. continue
  249. }
  250. report(defType.functionBody, prop, typeNames)
  251. } else {
  252. propContexts.push({
  253. prop,
  254. type: typeNames,
  255. default: defType
  256. })
  257. }
  258. }
  259. }
  260. vueObjectPropsContexts.set(obj, propContexts)
  261. },
  262. /**
  263. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  264. * @param {VueObjectData} data
  265. */
  266. ':function'(node, { node: vueNode }) {
  267. scopeStack = {
  268. upper: scopeStack,
  269. body: node.body,
  270. returnTypes: null
  271. }
  272. const data = vueObjectPropsContexts.get(vueNode)
  273. if (!data) {
  274. return
  275. }
  276. for (const { default: defType } of data) {
  277. if (node.body === defType.functionBody) {
  278. scopeStack.returnTypes = defType.returnTypes
  279. }
  280. }
  281. },
  282. /**
  283. * @param {ReturnStatement} node
  284. */
  285. ReturnStatement(node) {
  286. if (!scopeStack) {
  287. return
  288. }
  289. if (scopeStack.returnTypes && node.argument) {
  290. const type = getValueType(node.argument)
  291. if (type) {
  292. scopeStack.returnTypes.push({
  293. type: type.type,
  294. node: node.argument
  295. })
  296. }
  297. }
  298. },
  299. ':function:exit': onFunctionExit,
  300. onVueObjectExit(obj) {
  301. const data = vueObjectPropsContexts.get(obj)
  302. if (!data) {
  303. return
  304. }
  305. for (const { prop, type: typeNames, default: defType } of data) {
  306. for (const returnType of defType.returnTypes) {
  307. if (typeNames.has(returnType.type)) continue
  308. report(returnType.node, prop, typeNames)
  309. }
  310. }
  311. }
  312. })
  313. }
  314. }