Skip to content

Commit

Permalink
Update vue/require-valid-default-prop rule to support `<script setu…
Browse files Browse the repository at this point in the history
…p>` (#1538)
  • Loading branch information
ota-meshi committed Jul 3, 2021
1 parent ee5ea4b commit edd2248
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 102 deletions.
301 changes: 199 additions & 102 deletions lib/rules/require-valid-default-prop.js
Expand Up @@ -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
*/

Expand Down Expand Up @@ -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<string>, default: FunctionValueType } } PropDefaultFunctionContext
* @typedef { { type: string, node: Expression } } ReturnType
*/
/**
* @typedef {object} PropDefaultFunctionContext
* @property {ComponentObjectProp | ComponentTypeProp} 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
Expand Down Expand Up @@ -194,7 +222,7 @@ module.exports = {

/**
* @param {*} node
* @param {ComponentObjectProp} prop
* @param {ComponentObjectProp | ComponentTypeProp} prop
* @param {Iterable<string>} expectedTypeNames
*/
function report(node, prop, expectedTypeNames) {
Expand All @@ -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()
}
}
})
})
)
}
}

0 comments on commit edd2248

Please sign in to comment.