fix(compiler-sfc): handle type merging + fix namespace access when inferring type

close #8102
This commit is contained in:
Evan You 2023-04-20 14:13:08 +08:00
parent 5510ce385a
commit d53e157805
2 changed files with 229 additions and 37 deletions

View File

@ -294,6 +294,84 @@ describe('resolveType', () => {
}) })
}) })
test('interface merging', () => {
expect(
resolve(`
interface Foo {
a: string
}
interface Foo {
b: number
}
defineProps<{
foo: Foo['a'],
bar: Foo['b']
}>()
`).props
).toStrictEqual({
foo: ['String'],
bar: ['Number']
})
})
test('namespace merging', () => {
expect(
resolve(`
namespace Foo {
export type A = string
}
namespace Foo {
export type B = number
}
defineProps<{
foo: Foo.A,
bar: Foo.B
}>()
`).props
).toStrictEqual({
foo: ['String'],
bar: ['Number']
})
})
test('namespace merging with other types', () => {
expect(
resolve(`
namespace Foo {
export type A = string
}
interface Foo {
b: number
}
defineProps<{
foo: Foo.A,
bar: Foo['b']
}>()
`).props
).toStrictEqual({
foo: ['String'],
bar: ['Number']
})
})
test('enum merging', () => {
expect(
resolve(`
enum Foo {
A = 1
}
enum Foo {
B = 'hi'
}
defineProps<{
foo: Foo
}>()
`).props
).toStrictEqual({
foo: ['Number', 'String']
})
})
describe('external type imports', () => { describe('external type imports', () => {
const files = { const files = {
'/foo.ts': 'export type P = { foo: number }', '/foo.ts': 'export type P = { foo: number }',
@ -436,6 +514,34 @@ describe('resolveType', () => {
}) })
expect(deps && [...deps]).toStrictEqual(Object.keys(files)) expect(deps && [...deps]).toStrictEqual(Object.keys(files))
}) })
test('global types with ambient references', () => {
const files = {
// with references
'/backend.d.ts': `
declare namespace App.Data {
export type AircraftData = {
id: string
manufacturer: App.Data.Listings.ManufacturerData
}
}
declare namespace App.Data.Listings {
export type ManufacturerData = {
id: string
}
}
`
}
const { props } = resolve(`defineProps<App.Data.AircraftData>()`, files, {
globalTypeFiles: Object.keys(files)
})
expect(props).toStrictEqual({
id: ['String'],
manufacturer: ['Object']
})
})
}) })
describe('errors', () => { describe('errors', () => {

View File

@ -65,11 +65,14 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext
type Import = Pick<ImportBinding, 'source' | 'imported'> type Import = Pick<ImportBinding, 'source' | 'imported'>
type ScopeTypeNode = Node & { interface WithScope {
// scope types always has ownerScope attached
_ownerScope: TypeScope _ownerScope: TypeScope
} }
// scope types always has ownerScope attached
type ScopeTypeNode = Node &
WithScope & { _ns?: TSModuleDeclaration & WithScope }
export interface TypeScope { export interface TypeScope {
filename: string filename: string
source: string source: string
@ -79,7 +82,7 @@ export interface TypeScope {
exportedTypes: Record<string, ScopeTypeNode> exportedTypes: Record<string, ScopeTypeNode>
} }
export interface WithScope { export interface MaybeWithScope {
_ownerScope?: TypeScope _ownerScope?: TypeScope
} }
@ -100,7 +103,7 @@ interface ResolvedElements {
*/ */
export function resolveTypeElements( export function resolveTypeElements(
ctx: TypeResolveContext, ctx: TypeResolveContext,
node: Node & WithScope & { _resolvedElements?: ResolvedElements }, node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
scope?: TypeScope scope?: TypeScope
): ResolvedElements { ): ResolvedElements {
if (node._resolvedElements) { if (node._resolvedElements) {
@ -177,7 +180,7 @@ function typeElementsToMap(
const res: ResolvedElements = { props: {} } 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') {
;(e as WithScope)._ownerScope = scope ;(e as MaybeWithScope)._ownerScope = scope
const name = getId(e.key) const name = getId(e.key)
if (name && !e.computed) { if (name && !e.computed) {
res.props[name] = e as ResolvedElements['props'][string] res.props[name] = e as ResolvedElements['props'][string]
@ -248,7 +251,7 @@ function createProperty(
function resolveInterfaceMembers( function resolveInterfaceMembers(
ctx: TypeResolveContext, ctx: TypeResolveContext,
node: TSInterfaceDeclaration & WithScope, node: TSInterfaceDeclaration & MaybeWithScope,
scope: TypeScope scope: TypeScope
): ResolvedElements { ): ResolvedElements {
const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
@ -289,7 +292,7 @@ function resolveIndexType(
ctx: TypeResolveContext, ctx: TypeResolveContext,
node: TSIndexedAccessType, node: TSIndexedAccessType,
scope: TypeScope scope: TypeScope
): (TSType & WithScope)[] { ): (TSType & MaybeWithScope)[] {
if (node.indexType.type === 'TSNumberKeyword') { if (node.indexType.type === 'TSNumberKeyword') {
return resolveArrayElementType(ctx, node.objectType, scope) return resolveArrayElementType(ctx, node.objectType, scope)
} }
@ -308,7 +311,7 @@ function resolveIndexType(
for (const key of keys) { for (const key of keys) {
const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation
if (targetType) { if (targetType) {
;(targetType as TSType & WithScope)._ownerScope = ;(targetType as TSType & MaybeWithScope)._ownerScope =
resolved.props[key]._ownerScope resolved.props[key]._ownerScope
types.push(targetType) types.push(targetType)
} }
@ -532,15 +535,14 @@ function innerResolveTypeReference(
} }
} }
} else { } else {
const ns = innerResolveTypeReference( let ns = innerResolveTypeReference(ctx, scope, name[0], node, onlyExported)
ctx, if (ns) {
scope, if (ns.type !== 'TSModuleDeclaration') {
name[0], // namespace merged with other types, attached as _ns
node, ns = ns._ns
onlyExported }
) if (ns) {
if (ns && ns.type === 'TSModuleDeclaration') { const childScope = moduleDeclToScope(ns, ns._ownerScope || scope)
const childScope = moduleDeclToScope(ns, scope)
return innerResolveTypeReference( return innerResolveTypeReference(
ctx, ctx,
childScope, childScope,
@ -551,6 +553,7 @@ function innerResolveTypeReference(
} }
} }
} }
}
function getReferenceName( function getReferenceName(
node: TSTypeReference | TSExpressionWithTypeArguments node: TSTypeReference | TSExpressionWithTypeArguments
@ -771,7 +774,6 @@ export function fileToScope(
exportedTypes: Object.create(null) exportedTypes: Object.create(null)
} }
recordTypes(body, scope, asGlobal) recordTypes(body, scope, asGlobal)
fileToScopeCache.set(filename, scope) fileToScopeCache.set(filename, scope)
return scope return scope
} }
@ -858,10 +860,21 @@ function moduleDeclToScope(
} }
const scope: TypeScope = { const scope: TypeScope = {
...parentScope, ...parentScope,
imports: Object.create(parentScope.imports),
// TODO this seems wrong
types: Object.create(parentScope.types), types: Object.create(parentScope.types),
imports: Object.create(parentScope.imports) exportedTypes: Object.create(null)
} }
recordTypes((node.body as TSModuleBlock).body, scope)
if (node.body.type === 'TSModuleDeclaration') {
const decl = node.body as TSModuleDeclaration & WithScope
decl._ownerScope = scope
const id = getId(decl.id)
scope.types[id] = scope.exportedTypes[id] = decl
} else {
recordTypes(node.body.body, scope)
}
return (node._resolvedChildScope = scope) return (node._resolvedChildScope = scope)
} }
@ -923,7 +936,9 @@ function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) {
} }
} }
for (const key of Object.keys(types)) { for (const key of Object.keys(types)) {
types[key]._ownerScope = scope const node = types[key]
node._ownerScope = scope
if (node._ns) node._ns._ownerScope = scope
} }
} }
@ -931,12 +946,42 @@ function recordType(node: Node, types: Record<string, Node>) {
switch (node.type) { switch (node.type) {
case 'TSInterfaceDeclaration': case 'TSInterfaceDeclaration':
case 'TSEnumDeclaration': case 'TSEnumDeclaration':
case 'TSModuleDeclaration': case 'TSModuleDeclaration': {
case 'ClassDeclaration': { const id = getId(node.id)
const id = node.id.type === 'Identifier' ? node.id.name : node.id.value let existing = types[id]
types[id] = node if (existing) {
if (node.type === 'TSModuleDeclaration') {
if (existing.type === 'TSModuleDeclaration') {
mergeNamespaces(existing as typeof node, node)
} else {
attachNamespace(existing, node)
}
break break
} }
if (existing.type === 'TSModuleDeclaration') {
// replace and attach namespace
types[id] = node
attachNamespace(node, existing)
break
}
if (existing.type !== node.type) {
// type-level error
break
}
if (node.type === 'TSInterfaceDeclaration') {
;(existing as typeof node).body.body.push(...node.body.body)
} else {
;(existing as typeof node).members.push(...node.members)
}
} else {
types[id] = node
}
break
}
case 'ClassDeclaration':
types[getId(node.id)] = node
break
case 'TSTypeAliasDeclaration': case 'TSTypeAliasDeclaration':
types[node.id.name] = node.typeAnnotation types[node.id.name] = node.typeAnnotation
break break
@ -955,6 +1000,47 @@ function recordType(node: Node, types: Record<string, Node>) {
} }
} }
function mergeNamespaces(to: TSModuleDeclaration, from: TSModuleDeclaration) {
const toBody = to.body
const fromBody = from.body
if (toBody.type === 'TSModuleDeclaration') {
if (fromBody.type === 'TSModuleDeclaration') {
// both decl
mergeNamespaces(toBody, fromBody)
} else {
// to: decl -> from: block
fromBody.body.push({
type: 'ExportNamedDeclaration',
declaration: toBody,
exportKind: 'type',
specifiers: []
})
}
} else if (fromBody.type === 'TSModuleDeclaration') {
// to: block <- from: decl
toBody.body.push({
type: 'ExportNamedDeclaration',
declaration: fromBody,
exportKind: 'type',
specifiers: []
})
} else {
// both block
toBody.body.push(...fromBody.body)
}
}
function attachNamespace(
to: Node & { _ns?: TSModuleDeclaration },
ns: TSModuleDeclaration
) {
if (!to._ns) {
to._ns = ns
} else {
mergeNamespaces(to._ns, ns)
}
}
export function recordImports(body: Statement[]) { export function recordImports(body: Statement[]) {
const imports: TypeScope['imports'] = Object.create(null) const imports: TypeScope['imports'] = Object.create(null)
for (const s of body) { for (const s of body) {
@ -977,7 +1063,7 @@ function recordImport(node: Node, imports: TypeScope['imports']) {
export function inferRuntimeType( export function inferRuntimeType(
ctx: TypeResolveContext, ctx: TypeResolveContext,
node: Node & WithScope, node: Node & MaybeWithScope,
scope = node._ownerScope || ctxToScope(ctx) scope = node._ownerScope || ctxToScope(ctx)
): string[] { ): string[] {
switch (node.type) { switch (node.type) {
@ -1035,11 +1121,11 @@ export function inferRuntimeType(
} }
case 'TSTypeReference': case 'TSTypeReference':
if (node.typeName.type === 'Identifier') {
const resolved = resolveTypeReference(ctx, node, scope) const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) { if (resolved) {
return inferRuntimeType(ctx, resolved, resolved._ownerScope) return inferRuntimeType(ctx, resolved, resolved._ownerScope)
} }
if (node.typeName.type === 'Identifier') {
switch (node.typeName.name) { switch (node.typeName.name) {
case 'Array': case 'Array':
case 'Function': case 'Function':