123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- /**
- * @fileoverview Enforces props default values to be valid.
- * @author Armano
- */
- 'use strict'
- const utils = require('../utils')
- const { capitalize } = require('../utils/casing')
- /**
- * @typedef {import('../utils').ComponentProp} ComponentProp
- * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
- * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
- * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp
- * @typedef {import('../utils').ComponentInferTypeProp} ComponentInferTypeProp
- * @typedef {import('../utils').ComponentUnknownProp} ComponentUnknownProp
- * @typedef {import('../utils').VueObjectData} VueObjectData
- */
- const NATIVE_TYPES = new Set([
- 'String',
- 'Number',
- 'Boolean',
- 'Function',
- 'Object',
- 'Array',
- 'Symbol',
- 'BigInt'
- ])
- const FUNCTION_VALUE_TYPES = new Set(['Function', 'Object', 'Array'])
- /**
- * @param {ObjectExpression} obj
- * @param {string} name
- * @returns {Property | null}
- */
- function getPropertyNode(obj, name) {
- for (const p of obj.properties) {
- if (
- p.type === 'Property' &&
- !p.computed &&
- p.key.type === 'Identifier' &&
- p.key.name === name
- ) {
- return p
- }
- }
- return null
- }
- /**
- * @param {Expression} targetNode
- * @returns {string[]}
- */
- function getTypes(targetNode) {
- const node = utils.skipTSAsExpression(targetNode)
- if (node.type === 'Identifier') {
- return [node.name]
- } else if (node.type === 'ArrayExpression') {
- return node.elements
- .filter(
- /**
- * @param {Expression | SpreadElement | null} item
- * @returns {item is Identifier}
- */
- (item) => item != null && item.type === 'Identifier'
- )
- .map((item) => item.name)
- }
- return []
- }
- module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description: 'enforce props default values to be valid',
- categories: ['vue3-essential', 'vue2-essential'],
- url: 'https://eslint.vuejs.org/rules/require-valid-default-prop.html'
- },
- fixable: null,
- schema: [],
- messages: {
- invalidType:
- "Type of the default value for '{{name}}' prop must be a {{types}}."
- }
- },
- /** @param {RuleContext} context */
- create(context) {
- /**
- * @typedef {object} StandardValueType
- * @property {string} type
- * @property {false} function
- */
- /**
- * @typedef {object} FunctionExprValueType
- * @property {'Function'} type
- * @property {true} function
- * @property {true} expression
- * @property {Expression} functionBody
- * @property {string | null} returnType
- */
- /**
- * @typedef {object} FunctionValueType
- * @property {'Function'} type
- * @property {true} function
- * @property {false} expression
- * @property {BlockStatement} functionBody
- * @property {ReturnType[]} returnTypes
- */
- /**
- * @typedef { ComponentObjectProp & { value: ObjectExpression } } ComponentObjectDefineProp
- * @typedef { { type: string, node: Expression } } ReturnType
- */
- /**
- * @typedef {object} PropDefaultFunctionContext
- * @property {ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp} prop
- * @property {Set<string>} types
- * @property {FunctionValueType} default
- */
- /**
- * @type {Map<ObjectExpression, PropDefaultFunctionContext[]>}
- */
- const vueObjectPropsContexts = new Map()
- /**
- * @type { {node: CallExpression, props:PropDefaultFunctionContext[]}[] }
- */
- const scriptSetupPropsContexts = []
- /**
- * @typedef {object} ScopeStack
- * @property {ScopeStack | null} upper
- * @property {BlockStatement | Expression} body
- * @property {null | ReturnType[]} [returnTypes]
- */
- /**
- * @type {ScopeStack | null}
- */
- let scopeStack = null
- function onFunctionExit() {
- scopeStack = scopeStack && scopeStack.upper
- }
- /**
- * @param {Expression} targetNode
- * @returns { StandardValueType | FunctionExprValueType | FunctionValueType | null }
- */
- function getValueType(targetNode) {
- const node = utils.skipChainExpression(targetNode)
- switch (node.type) {
- case 'CallExpression': {
- // Symbol(), Number() ...
- if (
- node.callee.type === 'Identifier' &&
- NATIVE_TYPES.has(node.callee.name)
- ) {
- return {
- function: false,
- type: node.callee.name
- }
- }
- break
- }
- case 'TemplateLiteral': {
- // String
- return {
- function: false,
- type: 'String'
- }
- }
- case 'Literal': {
- // String, Boolean, Number
- if (node.value === null && !node.bigint) return null
- const type = node.bigint ? 'BigInt' : capitalize(typeof node.value)
- if (NATIVE_TYPES.has(type)) {
- return {
- function: false,
- type
- }
- }
- break
- }
- case 'ArrayExpression': {
- // Array
- return {
- function: false,
- type: 'Array'
- }
- }
- case 'ObjectExpression': {
- // Object
- return {
- function: false,
- type: 'Object'
- }
- }
- case 'FunctionExpression': {
- return {
- function: true,
- expression: false,
- type: 'Function',
- functionBody: node.body,
- returnTypes: []
- }
- }
- case 'ArrowFunctionExpression': {
- if (node.expression) {
- const valueType = getValueType(node.body)
- return {
- function: true,
- expression: true,
- type: 'Function',
- functionBody: node.body,
- returnType: valueType ? valueType.type : null
- }
- }
- return {
- function: true,
- expression: false,
- type: 'Function',
- functionBody: node.body,
- returnTypes: []
- }
- }
- }
- return null
- }
- /**
- * @param {Expression} node
- * @param {ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp} prop
- * @param {Iterable<string>} expectedTypeNames
- */
- function report(node, prop, expectedTypeNames) {
- const propName =
- prop.propName == null
- ? `[${context.getSourceCode().getText(prop.node.key)}]`
- : prop.propName
- context.report({
- node,
- messageId: 'invalidType',
- data: {
- name: propName,
- types: [...expectedTypeNames].join(' or ').toLowerCase()
- }
- })
- }
- /**
- * @typedef {object} DefaultDefine
- * @property {Expression} expression
- * @property {'assignment'|'withDefaults'|'defaultProperty'} src
- */
- /**
- * @param {(ComponentObjectProp | ComponentTypeProp | ComponentInferTypeProp)[]} props
- * @param {(propName: string) => Iterable<DefaultDefine>} otherDefaultProvider
- */
- function processPropDefs(props, otherDefaultProvider) {
- /** @type {PropDefaultFunctionContext[]} */
- const propContexts = []
- for (const prop of props) {
- let typeList
- /** @type {DefaultDefine[]} */
- const defaultList = []
- if (prop.type === 'object') {
- if (prop.value.type === 'ObjectExpression') {
- const type = getPropertyNode(prop.value, 'type')
- if (!type) continue
- typeList = getTypes(type.value)
- const def = getPropertyNode(prop.value, 'default')
- if (def) {
- defaultList.push({
- src: 'defaultProperty',
- expression: def.value
- })
- }
- } else {
- typeList = getTypes(prop.value)
- }
- } else {
- typeList = prop.types
- }
- if (prop.propName != null) {
- defaultList.push(...otherDefaultProvider(prop.propName))
- }
- if (defaultList.length === 0) continue
- const typeNames = new Set(
- typeList.filter((item) => NATIVE_TYPES.has(item))
- )
- // There is no native types detected
- if (typeNames.size === 0) continue
- for (const defaultDef of defaultList) {
- const defType = getValueType(defaultDef.expression)
- if (!defType) continue
- if (defType.function) {
- if (typeNames.has('Function')) {
- continue
- }
- if (defaultDef.src === 'assignment') {
- // Factory functions cannot be used in default definitions with initial value assignments.
- report(defaultDef.expression, prop, typeNames)
- continue
- }
- if (defType.expression) {
- if (!defType.returnType || typeNames.has(defType.returnType)) {
- continue
- }
- report(defType.functionBody, prop, typeNames)
- } else {
- propContexts.push({
- prop,
- types: typeNames,
- default: defType
- })
- }
- } else {
- if (typeNames.has(defType.type)) {
- if (defaultDef.src === 'assignment') {
- continue
- }
- if (!FUNCTION_VALUE_TYPES.has(defType.type)) {
- // For Array and Object, defaults must be defined in the factory function.
- continue
- }
- }
- report(
- defaultDef.expression,
- prop,
- defaultDef.src === 'assignment'
- ? typeNames
- : [...typeNames].map((type) =>
- FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
- )
- )
- }
- }
- }
- return propContexts
- }
- return utils.compositingVisitors(
- {
- /**
- * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
- */
- ':function'(node) {
- scopeStack = {
- upper: scopeStack,
- body: node.body,
- returnTypes: null
- }
- },
- /**
- * @param {ReturnStatement} node
- */
- ReturnStatement(node) {
- if (!scopeStack) {
- return
- }
- if (scopeStack.returnTypes && node.argument) {
- const type = getValueType(node.argument)
- if (type) {
- scopeStack.returnTypes.push({
- type: type.type,
- node: node.argument
- })
- }
- }
- },
- ':function:exit': onFunctionExit
- },
- utils.defineVueVisitor(context, {
- onVueObjectEnter(obj) {
- /** @type {ComponentObjectDefineProp[]} */
- const props = utils.getComponentPropsFromOptions(obj).filter(
- /**
- * @param {ComponentObjectProp | ComponentArrayProp | ComponentUnknownProp} prop
- * @returns {prop is ComponentObjectDefineProp}
- */
- (prop) =>
- Boolean(
- prop.type === 'object' && prop.value.type === 'ObjectExpression'
- )
- )
- const propContexts = processPropDefs(props, () => [])
- vueObjectPropsContexts.set(obj, propContexts)
- },
- /**
- * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
- * @param {VueObjectData} data
- */
- ':function'(node, { node: vueNode }) {
- const data = vueObjectPropsContexts.get(vueNode)
- if (!data || !scopeStack) {
- return
- }
- for (const { default: defType } of data) {
- if (node.body === defType.functionBody) {
- scopeStack.returnTypes = defType.returnTypes
- }
- }
- },
- onVueObjectExit(obj) {
- const data = vueObjectPropsContexts.get(obj)
- if (!data) {
- return
- }
- for (const { prop, types: typeNames, default: defType } of data) {
- for (const returnType of defType.returnTypes) {
- if (typeNames.has(returnType.type)) continue
- report(returnType.node, prop, typeNames)
- }
- }
- }
- }),
- utils.defineScriptSetupVisitor(context, {
- onDefinePropsEnter(node, baseProps) {
- const props = baseProps.filter(
- /**
- * @param {ComponentProp} prop
- * @returns {prop is ComponentObjectProp | ComponentInferTypeProp | ComponentTypeProp}
- */
- (prop) =>
- Boolean(
- prop.type === 'type' ||
- prop.type === 'infer-type' ||
- prop.type === 'object'
- )
- )
- const defaultsByWithDefaults =
- utils.getWithDefaultsPropExpressions(node)
- const defaultsByAssignmentPatterns =
- utils.getDefaultPropExpressionsForPropsDestructure(node)
- const propContexts = processPropDefs(props, function* (propName) {
- const withDefaults = defaultsByWithDefaults[propName]
- if (withDefaults) {
- yield { src: 'withDefaults', expression: withDefaults }
- }
- const assignmentPattern = defaultsByAssignmentPatterns[propName]
- if (assignmentPattern) {
- yield {
- src: 'assignment',
- expression: assignmentPattern.expression
- }
- }
- })
- scriptSetupPropsContexts.push({ node, props: propContexts })
- },
- /**
- * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
- */
- ':function'(node) {
- const data =
- scriptSetupPropsContexts[scriptSetupPropsContexts.length - 1]
- if (!data || !scopeStack) {
- return
- }
- for (const { default: defType } of data.props) {
- if (node.body === defType.functionBody) {
- scopeStack.returnTypes = defType.returnTypes
- }
- }
- },
- onDefinePropsExit() {
- const data = scriptSetupPropsContexts.pop()
- if (!data) {
- return
- }
- for (const {
- prop,
- types: typeNames,
- default: defType
- } of data.props) {
- for (const returnType of defType.returnTypes) {
- if (typeNames.has(returnType.type)) continue
- report(returnType.node, prop, typeNames)
- }
- }
- }
- })
- )
- }
- }
|