123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- /**
- * @fileoverview Enforces props default values to be valid.
- * @author Armano
- */
- 'use strict'
- const utils = require('../utils')
- const { capitalize } = require('../utils/casing')
- /**
- * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
- * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp
- * @typedef {import('../utils').VueObjectData} VueObjectData
- */
- // ----------------------------------------------------------------------
- // Helpers
- // ----------------------------------------------------------------------
- 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} node
- * @returns {string[]}
- */
- function getTypes(node) {
- 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 []
- }
- // ------------------------------------------------------------------------------
- // Rule Definition
- // ------------------------------------------------------------------------------
- module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description: 'enforce props default values to be valid',
- categories: ['vue3-essential', 'essential'],
- url: 'https://eslint.vuejs.org/rules/require-valid-default-prop.html'
- },
- fixable: null,
- schema: []
- },
- /** @param {RuleContext} context */
- create(context) {
- /**
- * @typedef { { type: string, function: false } } StandardValueType
- * @typedef { { type: 'Function', function: true, expression: true, functionBody: Expression, returnType: string | null } } FunctionExprValueType
- * @typedef { { type: 'Function', function: true, expression: false, functionBody: BlockStatement, returnTypes: ReturnType[] } } FunctionValueType
- * @typedef { ComponentObjectProp & { value: ObjectExpression } } ComponentObjectDefineProp
- * @typedef { { prop: ComponentObjectDefineProp, type: Set<string>, default: FunctionValueType } } PropDefaultFunctionContext
- * @typedef { { type: string, node: Expression } } ReturnType
- */
- /**
- * @type {Map<ObjectExpression, PropDefaultFunctionContext[]>}
- */
- const vueObjectPropsContexts = new Map()
- /**
- * @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)
- if (node.type === 'CallExpression') {
- // Symbol(), Number() ...
- if (
- node.callee.type === 'Identifier' &&
- NATIVE_TYPES.has(node.callee.name)
- ) {
- return {
- function: false,
- type: node.callee.name
- }
- }
- } else if (node.type === 'TemplateLiteral') {
- // String
- return {
- function: false,
- type: 'String'
- }
- } else if (node.type === '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
- }
- }
- } else if (node.type === 'ArrayExpression') {
- // Array
- return {
- function: false,
- type: 'Array'
- }
- } else if (node.type === 'ObjectExpression') {
- // Object
- return {
- function: false,
- type: 'Object'
- }
- } else if (node.type === 'FunctionExpression') {
- return {
- function: true,
- expression: false,
- type: 'Function',
- functionBody: node.body,
- returnTypes: []
- }
- } else if (node.type === 'ArrowFunctionExpression') {
- if (node.expression) {
- const valueType = getValueType(node.body)
- return {
- function: true,
- expression: true,
- type: 'Function',
- functionBody: node.body,
- returnType: valueType ? valueType.type : null
- }
- } else {
- return {
- function: true,
- expression: false,
- type: 'Function',
- functionBody: node.body,
- returnTypes: []
- }
- }
- }
- return null
- }
- /**
- * @param {*} node
- * @param {ComponentObjectProp} prop
- * @param {Iterable<string>} expectedTypeNames
- */
- function report(node, prop, expectedTypeNames) {
- const propName =
- prop.propName != null
- ? prop.propName
- : `[${context.getSourceCode().getText(prop.node.key)}]`
- context.report({
- node,
- message:
- "Type of the default value for '{{name}}' prop must be a {{types}}.",
- data: {
- name: propName,
- types: Array.from(expectedTypeNames).join(' or ').toLowerCase()
- }
- })
- }
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
- return utils.defineVueVisitor(context, {
- onVueObjectEnter(obj) {
- /** @type {ComponentObjectDefineProp[]} */
- const props = utils.getComponentProps(obj).filter(
- /**
- * @param {ComponentObjectProp | ComponentArrayProp} prop
- * @returns {prop is ComponentObjectDefineProp}
- */
- (prop) =>
- Boolean(prop.value && prop.value.type === 'ObjectExpression')
- )
- /** @type {PropDefaultFunctionContext[]} */
- const propContexts = []
- for (const prop of props) {
- const type = getPropertyNode(prop.value, 'type')
- if (!type) continue
- const typeNames = new Set(
- getTypes(type.value).filter((item) => NATIVE_TYPES.has(item))
- )
- // There is no native types detected
- if (typeNames.size === 0) continue
- const def = getPropertyNode(prop.value, 'default')
- if (!def) continue
- const defType = getValueType(def.value)
- if (!defType) continue
- if (!defType.function) {
- if (typeNames.has(defType.type)) {
- if (!FUNCTION_VALUE_TYPES.has(defType.type)) {
- continue
- }
- }
- report(
- def.value,
- prop,
- Array.from(typeNames).map((type) =>
- FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type
- )
- )
- } else {
- if (typeNames.has('Function')) {
- continue
- }
- if (defType.expression) {
- if (!defType.returnType || typeNames.has(defType.returnType)) {
- continue
- }
- report(defType.functionBody, prop, typeNames)
- } else {
- propContexts.push({
- prop,
- type: typeNames,
- default: defType
- })
- }
- }
- }
- vueObjectPropsContexts.set(obj, propContexts)
- },
- /**
- * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
- * @param {VueObjectData} data
- */
- ':function'(node, { node: vueNode }) {
- scopeStack = {
- upper: scopeStack,
- body: node.body,
- returnTypes: null
- }
- const data = vueObjectPropsContexts.get(vueNode)
- if (!data) {
- return
- }
- for (const { default: defType } of data) {
- if (node.body === defType.functionBody) {
- scopeStack.returnTypes = defType.returnTypes
- }
- }
- },
- /**
- * @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,
- onVueObjectExit(obj) {
- const data = vueObjectPropsContexts.get(obj)
- if (!data) {
- return
- }
- for (const { prop, type: typeNames, default: defType } of data) {
- for (const returnType of defType.returnTypes) {
- if (typeNames.has(returnType.type)) continue
- report(returnType.node, prop, typeNames)
- }
- }
- }
- })
- }
- }
|