UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

361 lines (358 loc) 12 kB
'use strict'; const require_rolldown_runtime = require('../_virtual/rolldown_runtime.js'); const require_index = require('../utils/index.js'); const require_casing$1 = require('../utils/casing.js'); //#region lib/rules/require-valid-default-prop.js /** * @fileoverview Enforces props default values to be valid. * @author Armano */ var require_require_valid_default_prop = /* @__PURE__ */ require_rolldown_runtime.__commonJSMin(((exports, module) => { const utils = require_index.default; const { capitalize } = require_casing$1.default; /** * @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}}." } }, 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 = /* @__PURE__ */ 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": if (node.callee.type === "Identifier" && NATIVE_TYPES.has(node.callee.name)) return { function: false, type: node.callee.name }; break; case "TemplateLiteral": return { function: false, type: "String" }; case "Literal": { 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": return { function: false, type: "Array" }; case "ObjectExpression": 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.sourceCode.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))); 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") { 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)) continue; } report(defaultDef.expression, prop, defaultDef.src === "assignment" ? typeNames : [...typeNames].map((type) => FUNCTION_VALUE_TYPES.has(type) ? "Function" : type)); } } } return propContexts; } return utils.compositingVisitors({ ":function"(node) { scopeStack = { upper: scopeStack, body: node.body, returnTypes: null }; }, 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) { const propContexts = processPropDefs(utils.getComponentPropsFromOptions(obj).filter( /** * @param {ComponentObjectProp | ComponentArrayProp | ComponentUnknownProp} prop * @returns {prop is ComponentObjectDefineProp} */ (prop) => Boolean(prop.type === "object" && prop.value.type === "ObjectExpression") ), () => []); vueObjectPropsContexts.set(obj, propContexts); }, ":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 }); }, ":function"(node) { const data = scriptSetupPropsContexts.at(-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); } } })); } }; })); //#endregion Object.defineProperty(exports, 'default', { enumerable: true, get: function () { return require_require_valid_default_prop(); } });