TSType,
TSTypeAnnotation,
TSTypeElement,
+ TSTypeLiteral,
+ TSTypeQuery,
TSTypeReference,
TemplateLiteral
} from '@babel/types'
imports: Record<string, Import>
types: Record<string, ScopeTypeNode>
exportedTypes: Record<string, ScopeTypeNode>
+ declares: Record<string, ScopeTypeNode>
+ exportedDeclares: Record<string, ScopeTypeNode>
}
export interface MaybeWithScope {
}
case 'TSExpressionWithTypeArguments': // referenced by interface extends
case 'TSTypeReference': {
+ const typeName = getReferenceName(node)
+ if (
+ typeName === 'ExtractPropTypes' &&
+ node.typeParameters &&
+ scope.imports[typeName]?.source === 'vue'
+ ) {
+ return resolveExtractPropTypes(
+ resolveTypeElements(ctx, node.typeParameters.params[0], scope),
+ scope
+ )
+ }
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
return resolveTypeElements(ctx, resolved, resolved._ownerScope)
} else {
- const typeName = getReferenceName(node)
- if (
- typeof typeName === 'string' &&
- // @ts-ignore
- SupportedBuiltinsSet.has(typeName)
- ) {
- return resolveBuiltin(ctx, node, typeName as any, scope)
+ if (typeof typeName === 'string') {
+ if (
+ // @ts-ignore
+ SupportedBuiltinsSet.has(typeName)
+ ) {
+ return resolveBuiltin(ctx, node, typeName as any, scope)
+ } else if (typeName === 'ReturnType' && node.typeParameters) {
+ // limited support, only reference types
+ const ret = resolveReturnType(
+ ctx,
+ node.typeParameters.params[0],
+ scope
+ )
+ if (ret) {
+ return resolveTypeElements(ctx, ret, scope)
+ }
+ }
}
return ctx.error(
`Unresolvable type reference or unsupported built-in utility type`,
)
}
}
- case 'TSImportType':
+ case 'TSImportType': {
+ if (
+ getId(node.argument) === 'vue' &&
+ node.qualifier?.type === 'Identifier' &&
+ node.qualifier.name === 'ExtractPropTypes' &&
+ node.typeParameters
+ ) {
+ return resolveExtractPropTypes(
+ resolveTypeElements(ctx, node.typeParameters.params[0], scope),
+ scope
+ )
+ }
const sourceScope = importSourceToScope(
ctx,
node.argument,
if (resolved) {
return resolveTypeElements(ctx, resolved, resolved._ownerScope)
}
+ }
+ case 'TSTypeQuery': {
+ const resolved = resolveTypeReference(ctx, node, scope)
+ if (resolved) {
+ return resolveTypeElements(ctx, resolved, resolved._ownerScope)
+ }
+ }
}
return ctx.error(`Unresolvable type: ${node.type}`, node, scope)
}
// @ts-ignore
types: [baseProps[key], props[key]]
},
- baseProps[key]._ownerScope
+ baseProps[key]._ownerScope,
+ baseProps[key].optional || props[key].optional
)
}
}
function createProperty(
key: Expression,
typeAnnotation: TSType,
- scope: TypeScope
-): TSPropertySignature & { _ownerScope: TypeScope } {
+ scope: TypeScope,
+ optional: boolean
+): TSPropertySignature & WithScope {
return {
type: 'TSPropertySignature',
key,
kind: 'get',
+ optional,
typeAnnotation: {
type: 'TSTypeAnnotation',
typeAnnotation
name: key
},
node.typeAnnotation!,
- scope
+ scope,
+ !!node.optional
)
}
return res
| TSTypeReference
| TSExpressionWithTypeArguments
| TSImportType
+ | TSTypeQuery
function resolveTypeReference(
ctx: TypeResolveContext,
if (scope.imports[name]) {
return resolveTypeFromImport(ctx, node, name, scope)
} else {
- const types = onlyExported ? scope.exportedTypes : scope.types
- if (types[name]) {
- return types[name]
+ const lookupSource =
+ node.type === 'TSTypeQuery'
+ ? onlyExported
+ ? scope.exportedDeclares
+ : scope.declares
+ : onlyExported
+ ? scope.exportedTypes
+ : scope.types
+ if (lookupSource[name]) {
+ return lookupSource[name]
} else {
// fallback to global
const globalScopes = resolveGlobalScope(ctx)
if (globalScopes) {
for (const s of globalScopes) {
- if (s.types[name]) {
+ const src = node.type === 'TSTypeQuery' ? s.declares : s.types
+ if (src[name]) {
;(ctx.deps || (ctx.deps = new Set())).add(s.filename)
- return s.types[name]
+ return src[name]
}
}
}
? node.typeName
: node.type === 'TSExpressionWithTypeArguments'
? node.expression
- : node.qualifier
- if (!ref) {
- return 'default'
- } else if (ref.type === 'Identifier') {
+ : node.type === 'TSImportType'
+ ? node.qualifier
+ : node.exprName
+ if (ref?.type === 'Identifier') {
return ref.name
- } else {
+ } else if (ref?.type === 'TSQualifiedName') {
return qualifiedNameToPath(ref)
+ } else {
+ return 'default'
}
}
offset: 0,
imports: recordImports(body),
types: Object.create(null),
- exportedTypes: Object.create(null)
+ exportedTypes: Object.create(null),
+ declares: Object.create(null),
+ exportedDeclares: Object.create(null)
}
recordTypes(ctx, body, scope, asGlobal)
fileToScopeCache.set(filename, scope)
? Object.create(ctx.userImports)
: recordImports(body),
types: Object.create(null),
- exportedTypes: Object.create(null)
+ exportedTypes: Object.create(null),
+ declares: Object.create(null),
+ exportedDeclares: Object.create(null)
}
recordTypes(ctx, body, scope)
const scope: TypeScope = {
...parentScope,
imports: Object.create(parentScope.imports),
- // TODO this seems wrong
types: Object.create(parentScope.types),
- exportedTypes: Object.create(null)
+ declares: Object.create(parentScope.declares),
+ exportedTypes: Object.create(null),
+ exportedDeclares: Object.create(null)
}
if (node.body.type === 'TSModuleDeclaration') {
scope: TypeScope,
asGlobal = false
) {
- const { types, exportedTypes, imports } = scope
+ const { types, declares, exportedTypes, exportedDeclares, imports } = scope
const isAmbient = asGlobal
? !body.some(s => importExportRE.test(s.type))
: false
if (asGlobal) {
if (isAmbient) {
if ((stmt as any).declare) {
- recordType(stmt, types)
+ recordType(stmt, types, declares)
}
} else if (stmt.type === 'TSModuleDeclaration' && stmt.global) {
for (const s of (stmt.body as TSModuleBlock).body) {
- recordType(s, types)
+ recordType(s, types, declares)
}
}
} else {
- recordType(stmt, types)
+ recordType(stmt, types, declares)
}
}
if (!asGlobal) {
for (const stmt of body) {
if (stmt.type === 'ExportNamedDeclaration') {
if (stmt.declaration) {
- recordType(stmt.declaration, types)
- recordType(stmt.declaration, exportedTypes)
+ recordType(stmt.declaration, types, declares)
+ recordType(stmt.declaration, exportedTypes, exportedDeclares)
} else {
for (const spec of stmt.specifiers) {
if (spec.type === 'ExportSpecifier') {
node._ownerScope = scope
if (node._ns) node._ns._ownerScope = scope
}
+ for (const key of Object.keys(declares)) {
+ declares[key]._ownerScope = scope
+ }
}
-function recordType(node: Node, types: Record<string, Node>) {
+function recordType(
+ node: Node,
+ types: Record<string, Node>,
+ declares: Record<string, Node>
+) {
switch (node.type) {
case 'TSInterfaceDeclaration':
case 'TSEnumDeclaration':
case 'TSTypeAliasDeclaration':
types[node.id.name] = node.typeAnnotation
break
+ case 'TSDeclareFunction':
+ if (node.id) declares[node.id.name] = node
+ break
case 'VariableDeclaration': {
if (node.declare) {
for (const decl of node.declarations) {
if (decl.id.type === 'Identifier' && decl.id.typeAnnotation) {
- types[decl.id.name] = (
+ declares[decl.id.name] = (
decl.id.typeAnnotation as TSTypeAnnotation
).typeAnnotation
}
}
}
// cannot infer, fallback to UNKNOWN: ThisParameterType
- return [UNKNOWN_TYPE]
+ break
}
case 'TSParenthesizedType':
try {
const types = resolveIndexType(ctx, node, scope)
return flattenTypes(ctx, types, scope)
- } catch (e) {}
+ } catch (e) {
+ break
+ }
}
case 'ClassDeclaration':
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
}
} catch (e) {}
+ break
}
- default:
- return [UNKNOWN_TYPE] // no runtime check
+ case 'TSTypeQuery': {
+ const id = node.exprName
+ if (id.type === 'Identifier') {
+ // typeof only support identifier in local scope
+ const matched = scope.declares[id.name]
+ if (matched) {
+ return inferRuntimeType(ctx, matched, matched._ownerScope)
+ }
+ }
+ break
+ }
}
+ return [UNKNOWN_TYPE] // no runtime check
}
function flattenTypes(
}
return types.size ? [...types] : ['Number']
}
+
+/**
+ * support for the `ExtractPropTypes` helper - it's non-exhaustive, mostly
+ * tailored towards popular component libs like element-plus and antd-vue.
+ */
+function resolveExtractPropTypes(
+ { props }: ResolvedElements,
+ scope: TypeScope
+): ResolvedElements {
+ const res: ResolvedElements = { props: {} }
+ for (const key in props) {
+ const raw = props[key]
+ res.props[key] = reverseInferType(
+ raw.key,
+ raw.typeAnnotation!.typeAnnotation,
+ scope
+ )
+ }
+ return res
+}
+
+function reverseInferType(
+ key: Expression,
+ node: TSType,
+ scope: TypeScope,
+ optional = true,
+ checkObjectSyntax = true
+): TSPropertySignature & WithScope {
+ if (checkObjectSyntax && node.type === 'TSTypeLiteral') {
+ // check { type: xxx }
+ const typeType = findStaticPropertyType(node, 'type')
+ if (typeType) {
+ const requiredType = findStaticPropertyType(node, 'required')
+ const optional =
+ requiredType &&
+ requiredType.type === 'TSLiteralType' &&
+ requiredType.literal.type === 'BooleanLiteral'
+ ? !requiredType.literal.value
+ : true
+ return reverseInferType(key, typeType, scope, optional, false)
+ }
+ } else if (
+ node.type === 'TSTypeReference' &&
+ node.typeName.type === 'Identifier'
+ ) {
+ if (node.typeName.name.endsWith('Constructor')) {
+ return createProperty(
+ key,
+ ctorToType(node.typeName.name),
+ scope,
+ optional
+ )
+ } else if (node.typeName.name === 'PropType' && node.typeParameters) {
+ // PropType<{}>
+ return createProperty(key, node.typeParameters.params[0], scope, optional)
+ }
+ }
+ if (
+ (node.type === 'TSTypeReference' || node.type === 'TSImportType') &&
+ node.typeParameters
+ ) {
+ // try if we can catch Foo.Bar<XXXConstructor>
+ for (const t of node.typeParameters.params) {
+ const inferred = reverseInferType(key, t, scope, optional)
+ if (inferred) return inferred
+ }
+ }
+ return createProperty(key, { type: `TSNullKeyword` }, scope, optional)
+}
+
+function ctorToType(ctorType: string): TSType {
+ const ctor = ctorType.slice(0, -11)
+ switch (ctor) {
+ case 'String':
+ case 'Number':
+ case 'Boolean':
+ return { type: `TS${ctor}Keyword` }
+ case 'Array':
+ case 'Function':
+ case 'Object':
+ case 'Set':
+ case 'Map':
+ case 'WeakSet':
+ case 'WeakMap':
+ case 'Date':
+ case 'Promise':
+ return {
+ type: 'TSTypeReference',
+ typeName: { type: 'Identifier', name: ctor }
+ }
+ }
+ // fallback to null
+ return { type: `TSNullKeyword` }
+}
+
+function findStaticPropertyType(node: TSTypeLiteral, key: string) {
+ const prop = node.members.find(
+ m =>
+ m.type === 'TSPropertySignature' &&
+ !m.computed &&
+ getId(m.key) === key &&
+ m.typeAnnotation
+ )
+ return prop && prop.typeAnnotation!.typeAnnotation
+}
+
+function resolveReturnType(
+ ctx: TypeResolveContext,
+ arg: Node,
+ scope: TypeScope
+) {
+ let resolved: Node | undefined = arg
+ if (
+ arg.type === 'TSTypeReference' ||
+ arg.type === 'TSTypeQuery' ||
+ arg.type === 'TSImportType'
+ ) {
+ resolved = resolveTypeReference(ctx, arg, scope)
+ }
+ if (!resolved) return
+ if (resolved.type === 'TSFunctionType') {
+ return resolved.typeAnnotation?.typeAnnotation
+ }
+ if (resolved.type === 'TSDeclareFunction') {
+ return resolved.returnType
+ }
+}