refactor: adjust ResolvedElements shape

This commit is contained in:
Evan You 2023-04-13 10:42:15 +08:00
parent 1cfab4c695
commit 51773d5d1d
4 changed files with 58 additions and 84 deletions

View File

@ -8,19 +8,19 @@ import {
describe('resolveType', () => { describe('resolveType', () => {
test('type literal', () => { test('type literal', () => {
const { elements, callSignatures } = resolve(`type Target = { const { props, calls } = resolve(`type Target = {
foo: number // property foo: number // property
bar(): void // method bar(): void // method
'baz': string // string literal key 'baz': string // string literal key
(e: 'foo'): void // call signature (e: 'foo'): void // call signature
(e: 'bar'): void (e: 'bar'): void
}`) }`)
expect(elements).toStrictEqual({ expect(props).toStrictEqual({
foo: ['Number'], foo: ['Number'],
bar: ['Function'], bar: ['Function'],
baz: ['String'] baz: ['String']
}) })
expect(callSignatures?.length).toBe(2) expect(calls?.length).toBe(2)
}) })
test('reference type', () => { test('reference type', () => {
@ -28,7 +28,7 @@ describe('resolveType', () => {
resolve(` resolve(`
type Aliased = { foo: number } type Aliased = { foo: number }
type Target = Aliased type Target = Aliased
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
foo: ['Number'] foo: ['Number']
}) })
@ -39,7 +39,7 @@ describe('resolveType', () => {
resolve(` resolve(`
export type Aliased = { foo: number } export type Aliased = { foo: number }
type Target = Aliased type Target = Aliased
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
foo: ['Number'] foo: ['Number']
}) })
@ -50,7 +50,7 @@ describe('resolveType', () => {
resolve(` resolve(`
interface Aliased { foo: number } interface Aliased { foo: number }
type Target = Aliased type Target = Aliased
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
foo: ['Number'] foo: ['Number']
}) })
@ -61,7 +61,7 @@ describe('resolveType', () => {
resolve(` resolve(`
export interface Aliased { foo: number } export interface Aliased { foo: number }
type Target = Aliased type Target = Aliased
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
foo: ['Number'] foo: ['Number']
}) })
@ -75,7 +75,7 @@ describe('resolveType', () => {
interface C { c: string } interface C { c: string }
interface Aliased extends B, C { foo: number } interface Aliased extends B, C { foo: number }
type Target = Aliased type Target = Aliased
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
a: ['Function'], a: ['Function'],
b: ['Boolean'], b: ['Boolean'],
@ -88,7 +88,7 @@ describe('resolveType', () => {
expect( expect(
resolve(` resolve(`
type Target = (e: 'foo') => void type Target = (e: 'foo') => void
`).callSignatures?.length `).calls?.length
).toBe(1) ).toBe(1)
}) })
@ -97,7 +97,7 @@ describe('resolveType', () => {
resolve(` resolve(`
type Fn = (e: 'foo') => void type Fn = (e: 'foo') => void
type Target = Fn type Target = Fn
`).callSignatures?.length `).calls?.length
).toBe(1) ).toBe(1)
}) })
@ -108,7 +108,7 @@ describe('resolveType', () => {
type Bar = { bar: string } type Bar = { bar: string }
type Baz = { bar: string | boolean } type Baz = { bar: string | boolean }
type Target = { self: any } & Foo & Bar & Baz type Target = { self: any } & Foo & Bar & Baz
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
self: ['Unknown'], self: ['Unknown'],
foo: ['Number'], foo: ['Number'],
@ -138,7 +138,7 @@ describe('resolveType', () => {
} }
type Target = CommonProps & ConditionalProps type Target = CommonProps & ConditionalProps
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
size: ['String'], size: ['String'],
color: ['String', 'Number'], color: ['String', 'Number'],
@ -155,7 +155,7 @@ describe('resolveType', () => {
type Target = { type Target = {
[\`_\${T}_\${S}_\`]: string [\`_\${T}_\${S}_\`]: string
} }
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
_foo_x_: ['String'], _foo_x_: ['String'],
_foo_y_: ['String'], _foo_y_: ['String'],
@ -177,7 +177,7 @@ describe('resolveType', () => {
} & { } & {
[K in \`x\${T}\`]: string [K in \`x\${T}\`]: string
} }
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
foo: ['String', 'Number'], foo: ['String', 'Number'],
bar: ['String', 'Number'], bar: ['String', 'Number'],
@ -196,7 +196,7 @@ describe('resolveType', () => {
type T = { foo: number, bar: string, baz: boolean } type T = { foo: number, bar: string, baz: boolean }
type K = 'foo' | 'bar' type K = 'foo' | 'bar'
type Target = Pick<T, K> type Target = Pick<T, K>
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
foo: ['Number'], foo: ['Number'],
bar: ['String'] bar: ['String']
@ -209,7 +209,7 @@ describe('resolveType', () => {
type T = { foo: number, bar: string, baz: boolean } type T = { foo: number, bar: string, baz: boolean }
type K = 'foo' | 'bar' type K = 'foo' | 'bar'
type Target = Omit<T, K> type Target = Omit<T, K>
`).elements `).props
).toStrictEqual({ ).toStrictEqual({
baz: ['Boolean'] baz: ['Boolean']
}) })
@ -231,13 +231,13 @@ function resolve(code: string) {
s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target' s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
) as TSTypeAliasDeclaration ) as TSTypeAliasDeclaration
const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation) const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation)
const elements: Record<string, string[]> = {} const props: Record<string, string[]> = {}
for (const key in raw) { for (const key in raw.props) {
elements[key] = inferRuntimeType(ctx, raw[key]) props[key] = inferRuntimeType(ctx, raw.props[key])
} }
return { return {
elements, props,
callSignatures: raw.__callSignatures, calls: raw.calls,
raw raw
} }
} }

View File

@ -69,22 +69,22 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set<string> {
return emits return emits
} }
const elements = resolveTypeElements(ctx, node) const { props, calls } = resolveTypeElements(ctx, node)
let hasProperty = false let hasProperty = false
for (const key in elements) { for (const key in props) {
emits.add(key) emits.add(key)
hasProperty = true hasProperty = true
} }
if (elements.__callSignatures) { if (calls) {
if (hasProperty) { if (hasProperty) {
ctx.error( ctx.error(
`defineEmits() type cannot mixed call signature and property syntax.`, `defineEmits() type cannot mixed call signature and property syntax.`,
node node
) )
} }
for (const call of elements.__callSignatures) { for (const call of calls) {
extractEventNames(call.parameters[0], emits) extractEventNames(call.parameters[0], emits)
} }
} }

View File

@ -191,8 +191,8 @@ function resolveRuntimePropsFromType(
): PropTypeData[] { ): PropTypeData[] {
const props: PropTypeData[] = [] const props: PropTypeData[] = []
const elements = resolveTypeElements(ctx, node) const elements = resolveTypeElements(ctx, node)
for (const key in elements) { for (const key in elements.props) {
const e = elements[key] const e = elements.props[key]
let type = inferRuntimeType(ctx, e) let type = inferRuntimeType(ctx, e)
let skipCheck = false let skipCheck = false
// skip check for result containing unknown types // skip check for result containing unknown types

View File

@ -18,7 +18,7 @@ import { UNKNOWN_TYPE } from './utils'
import { ScriptCompileContext } from './context' import { ScriptCompileContext } from './context'
import { ImportBinding } from '../compileScript' import { ImportBinding } from '../compileScript'
import { TSInterfaceDeclaration } from '@babel/types' import { TSInterfaceDeclaration } from '@babel/types'
import { capitalize, hasOwn, isArray } from '@vue/shared' import { capitalize, hasOwn } from '@vue/shared'
import { Expression } from '@babel/types' import { Expression } from '@babel/types'
export interface TypeScope { export interface TypeScope {
@ -28,11 +28,9 @@ export interface TypeScope {
types: Record<string, Node> types: Record<string, Node>
} }
type ResolvedElements = Record< interface ResolvedElements {
string, props: Record<string, TSPropertySignature | TSMethodSignature>
TSPropertySignature | TSMethodSignature calls?: (TSCallSignatureDeclaration | TSFunctionType)[]
> & {
__callSignatures?: (TSCallSignatureDeclaration | TSFunctionType)[]
} }
/** /**
@ -62,9 +60,7 @@ function innerResolveTypeElements(
case 'TSParenthesizedType': case 'TSParenthesizedType':
return resolveTypeElements(ctx, node.typeAnnotation) return resolveTypeElements(ctx, node.typeAnnotation)
case 'TSFunctionType': { case 'TSFunctionType': {
const ret: ResolvedElements = {} return { props: {}, calls: [node] }
addCallSignature(ret, node)
return ret
} }
case 'TSExpressionWithTypeArguments': // referenced by interface extends case 'TSExpressionWithTypeArguments': // referenced by interface extends
case 'TSTypeReference': { case 'TSTypeReference': {
@ -98,32 +94,11 @@ function innerResolveTypeElements(
ctx.error(`Unsupported type in SFC macro: ${node.type}`, node) ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
} }
function addCallSignature(
elements: ResolvedElements,
node:
| TSCallSignatureDeclaration
| TSFunctionType
| (TSCallSignatureDeclaration | TSFunctionType)[]
) {
if (!elements.__callSignatures) {
Object.defineProperty(elements, '__callSignatures', {
enumerable: false,
value: isArray(node) ? node : [node]
})
} else {
if (isArray(node)) {
elements.__callSignatures.push(...node)
} else {
elements.__callSignatures.push(node)
}
}
}
function typeElementsToMap( function typeElementsToMap(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
elements: TSTypeElement[] elements: TSTypeElement[]
): ResolvedElements { ): ResolvedElements {
const ret: ResolvedElements = {} const res: ResolvedElements = { props: {} }
for (const e of elements) { for (const e of elements) {
if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
const name = const name =
@ -133,10 +108,10 @@ function typeElementsToMap(
? e.key.value ? e.key.value
: null : null
if (name && !e.computed) { if (name && !e.computed) {
ret[name] = e res.props[name] = e
} else if (e.key.type === 'TemplateLiteral') { } else if (e.key.type === 'TemplateLiteral') {
for (const key of resolveTemplateKeys(ctx, e.key)) { for (const key of resolveTemplateKeys(ctx, e.key)) {
ret[key] = e res.props[key] = e
} }
} else { } else {
ctx.error( ctx.error(
@ -145,31 +120,32 @@ function typeElementsToMap(
) )
} }
} else if (e.type === 'TSCallSignatureDeclaration') { } else if (e.type === 'TSCallSignatureDeclaration') {
addCallSignature(ret, e) ;(res.calls || (res.calls = [])).push(e)
} }
} }
return ret return res
} }
function mergeElements( function mergeElements(
maps: ResolvedElements[], maps: ResolvedElements[],
type: 'TSUnionType' | 'TSIntersectionType' type: 'TSUnionType' | 'TSIntersectionType'
): ResolvedElements { ): ResolvedElements {
const res: ResolvedElements = Object.create(null) const res: ResolvedElements = { props: {} }
for (const m of maps) { const { props: baseProps } = res
for (const key in m) { for (const { props, calls } of maps) {
if (!(key in res)) { for (const key in props) {
res[key] = m[key] if (!hasOwn(baseProps, key)) {
baseProps[key] = props[key]
} else { } else {
res[key] = createProperty(res[key].key, { baseProps[key] = createProperty(baseProps[key].key, {
type, type,
// @ts-ignore // @ts-ignore
types: [res[key], m[key]] types: [baseProps[key], props[key]]
}) })
} }
} }
if (m.__callSignatures) { if (calls) {
addCallSignature(res, m.__callSignatures) ;(res.calls || (res.calls = [])).push(...calls)
} }
} }
return res return res
@ -197,10 +173,10 @@ function resolveInterfaceMembers(
const base = typeElementsToMap(ctx, node.body.body) const base = typeElementsToMap(ctx, node.body.body)
if (node.extends) { if (node.extends) {
for (const ext of node.extends) { for (const ext of node.extends) {
const resolvedExt = resolveTypeElements(ctx, ext) const { props } = resolveTypeElements(ctx, ext)
for (const key in resolvedExt) { for (const key in props) {
if (!hasOwn(base, key)) { if (!hasOwn(base.props, key)) {
base[key] = resolvedExt[key] base.props[key] = props[key]
} }
} }
} }
@ -212,13 +188,13 @@ function resolveMappedType(
ctx: ScriptCompileContext, ctx: ScriptCompileContext,
node: TSMappedType node: TSMappedType
): ResolvedElements { ): ResolvedElements {
const res: ResolvedElements = {} const res: ResolvedElements = { props: {} }
if (!node.typeParameter.constraint) { if (!node.typeParameter.constraint) {
ctx.error(`mapped type used in macros must have a finite constraint.`, node) ctx.error(`mapped type used in macros must have a finite constraint.`, node)
} }
const keys = resolveStringType(ctx, node.typeParameter.constraint) const keys = resolveStringType(ctx, node.typeParameter.constraint)
for (const key of keys) { for (const key of keys) {
res[key] = createProperty( res.props[key] = createProperty(
{ {
type: 'Identifier', type: 'Identifier',
name: key name: key
@ -323,20 +299,18 @@ function resolveBuiltin(
return t return t
case 'Pick': { case 'Pick': {
const picked = resolveStringType(ctx, node.typeParameters!.params[1]) const picked = resolveStringType(ctx, node.typeParameters!.params[1])
const res: ResolvedElements = {} const res: ResolvedElements = { props: {}, calls: t.calls }
if (t.__callSignatures) addCallSignature(res, t.__callSignatures)
for (const key of picked) { for (const key of picked) {
res[key] = t[key] res.props[key] = t.props[key]
} }
return res return res
} }
case 'Omit': case 'Omit':
const omitted = resolveStringType(ctx, node.typeParameters!.params[1]) const omitted = resolveStringType(ctx, node.typeParameters!.params[1])
const res: ResolvedElements = {} const res: ResolvedElements = { props: {}, calls: t.calls }
if (t.__callSignatures) addCallSignature(res, t.__callSignatures) for (const key in t.props) {
for (const key in t) {
if (!omitted.includes(key)) { if (!omitted.includes(key)) {
res[key] = t[key] res.props[key] = t.props[key]
} }
} }
return res return res