diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js index e40cad861..2b711b56c 100644 --- a/lib/rules/require-valid-default-prop.js +++ b/lib/rules/require-valid-default-prop.js @@ -9,6 +9,7 @@ const { capitalize } = require('../utils/casing') /** * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp * @typedef {import('../utils').ComponentArrayProp} ComponentArrayProp + * @typedef {import('../utils').ComponentTypeProp} ComponentTypeProp * @typedef {import('../utils').VueObjectData} VueObjectData */ @@ -88,18 +89,45 @@ module.exports = { /** @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 {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 { { prop: ComponentObjectDefineProp, type: Set, default: FunctionValueType } } PropDefaultFunctionContext * @typedef { { type: string, node: Expression } } ReturnType */ + /** + * @typedef {object} PropDefaultFunctionContext + * @property {ComponentObjectProp | ComponentTypeProp} prop + * @property {Set} types + * @property {FunctionValueType} default + */ /** * @type {Map} */ const vueObjectPropsContexts = new Map() + /** + * @type { {node: CallExpression, props:PropDefaultFunctionContext[]}[] } + */ + const scriptSetupPropsContexts = [] /** * @typedef {object} ScopeStack @@ -194,7 +222,7 @@ module.exports = { /** * @param {*} node - * @param {ComponentObjectProp} prop + * @param {ComponentObjectProp | ComponentTypeProp} prop * @param {Iterable} expectedTypeNames */ function report(node, prop, expectedTypeNames) { @@ -213,127 +241,196 @@ module.exports = { }) } - // ---------------------------------------------------------------------- - // 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) { + /** + * @param {(ComponentObjectDefineProp | ComponentTypeProp)[]} props + * @param { { [key: string]: Expression | undefined } } withDefaults + */ + function processPropDefs(props, withDefaults) { + /** @type {PropDefaultFunctionContext[]} */ + const propContexts = [] + for (const prop of props) { + let typeList + let defExpr + if (prop.type === 'object') { 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 + typeList = getTypes(type.value) const def = getPropertyNode(prop.value, 'default') if (!def) continue - const defType = getValueType(def.value) + defExpr = def.value + } else { + typeList = prop.types + defExpr = withDefaults[prop.propName] + } + if (!defExpr) continue + + const typeNames = new Set( + typeList.filter((item) => NATIVE_TYPES.has(item)) + ) + // There is no native types detected + if (typeNames.size === 0) continue + + const defType = getValueType(defExpr) - if (!defType) continue + if (!defType) continue - if (!defType.function) { - if (typeNames.has(defType.type)) { - if (!FUNCTION_VALUE_TYPES.has(defType.type)) { - 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 - ) + } + report( + defExpr, + prop, + Array.from(typeNames).map((type) => + FUNCTION_VALUE_TYPES.has(type) ? 'Function' : type ) - } else { - if (typeNames.has('Function')) { + ) + } else { + if (typeNames.has('Function')) { + continue + } + if (defType.expression) { + if (!defType.returnType || typeNames.has(defType.returnType)) { 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 + report(defType.functionBody, prop, typeNames) + } else { + propContexts.push({ + prop, + types: typeNames, + default: defType + }) + } + } + } + return propContexts + } + + // ---------------------------------------------------------------------- + // Public + // ---------------------------------------------------------------------- + + 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 }) } } - } - vueObjectPropsContexts.set(obj, propContexts) + }, + ':function:exit': onFunctionExit }, - /** - * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node - * @param {VueObjectData} data - */ - ':function'(node, { node: vueNode }) { - scopeStack = { - upper: scopeStack, - body: node.body, - returnTypes: null - } + utils.defineVueVisitor(context, { + onVueObjectEnter(obj) { + /** @type {ComponentObjectDefineProp[]} */ + const props = utils.getComponentProps(obj).filter( + /** + * @param {ComponentObjectProp | ComponentArrayProp} 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 + } - const data = vueObjectPropsContexts.get(vueNode) - if (!data) { - 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 - for (const { default: defType } of data) { - if (node.body === defType.functionBody) { - scopeStack.returnTypes = defType.returnTypes + report(returnType.node, prop, typeNames) + } } } - }, - /** - * @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 - }) + }), + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(node, baseProps) { + /** @type {(ComponentObjectDefineProp | ComponentTypeProp)[]} */ + const props = baseProps.filter( + /** + * @param {ComponentObjectProp | ComponentArrayProp | ComponentTypeProp} prop + * @returns {prop is ComponentObjectDefineProp | ComponentTypeProp} + */ + (prop) => + Boolean( + prop.type === 'type' || + (prop.type === 'object' && + prop.value.type === 'ObjectExpression') + ) + ) + const defaults = utils.getWithDefaultsPropExpressions(node) + const propContexts = processPropDefs(props, defaults) + scriptSetupPropsContexts.push({ node, props: propContexts }) + }, + /** + * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node + */ + ':function'(node) { + const data = + scriptSetupPropsContexts[scriptSetupPropsContexts.length - 1] + if (!data || !scopeStack) { + return } - } - }, - ':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) + for (const { default: defType } of data.props) { + if (node.body === defType.functionBody) { + scopeStack.returnTypes = defType.returnTypes + } } + }, + onDefinePropsExit(node) { + scriptSetupPropsContexts.pop() } - } - }) + }) + ) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index d259b486f..c41cd605f 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1105,11 +1105,25 @@ module.exports = { if (visitor.onDefinePropsEnter || visitor.onDefinePropsExit) { const definePropsMap = new Map() + + /** @type {ESNode | null} */ + let nested = null + scriptSetupVisitor[':function, BlockStatement'] = (node) => { + if (!nested) { + nested = node + } + } + scriptSetupVisitor[':function, BlockStatement:exit'] = (node) => { + if (nested === node) { + nested = null + } + } /** * @param {CallExpression} node */ scriptSetupVisitor.CallExpression = (node) => { if ( + !nested && inScriptSetup(node) && node.callee.type === 'Identifier' && node.callee.name === 'defineProps' @@ -1146,6 +1160,42 @@ module.exports = { return scriptSetupVisitor }, + /** + * Gets a map of the expressions defined in withDefaults. + * @param {CallExpression} node The node of defineProps + * @returns { { [key: string]: Expression | undefined } } + */ + getWithDefaultsPropExpressions(node) { + if ( + !node.parent || + node.parent.type !== 'CallExpression' || + node.parent.arguments[0] !== node || + node.parent.callee.type !== 'Identifier' || + node.parent.callee.name !== 'withDefaults' + ) { + return {} + } + const param = node.parent.arguments[1] + if (!param || param.type !== 'ObjectExpression') { + return {} + } + + /** @type {Record} */ + const result = {} + + for (const prop of param.properties) { + if (prop.type !== 'Property') { + return {} + } + const name = getStaticPropertyName(prop) + if (name != null) { + result[name] = prop.value + } + } + + return result + }, + getVueObjectType, /** * Get the Vue component definition type from given node diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js index 836f06d5c..e353edf58 100644 --- a/tests/lib/rules/require-valid-default-prop.js +++ b/tests/lib/rules/require-valid-default-prop.js @@ -871,6 +871,49 @@ ruleTester.run('require-valid-default-prop', rule, { parserOptions: { ecmaVersion: 6, sourceType: 'module' }, parser: require.resolve('@typescript-eslint/parser'), errors: errorMessage('function') + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + parser: require.resolve('vue-eslint-parser'), + errors: [ + { + message: "Type of the default value for 'foo' prop must be a string.", + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + parser: require.resolve('@typescript-eslint/parser') + }, + parser: require.resolve('vue-eslint-parser'), + errors: [ + { + message: "Type of the default value for 'foo' prop must be a string.", + line: 4 + } + ] } ] })