Skip to content

Commit

Permalink
fix(compiler-sfc): handle type merging + fix namespace access when in…
Browse files Browse the repository at this point in the history
…ferring type

close #8102
  • Loading branch information
yyx990803 committed Apr 20, 2023
1 parent 5510ce3 commit d53e157
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 37 deletions.
106 changes: 106 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
const files = {
'/foo.ts': 'export type P = { foo: number }',
Expand Down Expand Up @@ -436,6 +514,34 @@ describe('resolveType', () => {
})
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', () => {
Expand Down
160 changes: 123 additions & 37 deletions packages/compiler-sfc/src/script/resolveType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext

type Import = Pick<ImportBinding, 'source' | 'imported'>

type ScopeTypeNode = Node & {
// scope types always has ownerScope attached
interface WithScope {
_ownerScope: TypeScope
}

// scope types always has ownerScope attached
type ScopeTypeNode = Node &
WithScope & { _ns?: TSModuleDeclaration & WithScope }

export interface TypeScope {
filename: string
source: string
Expand All @@ -79,7 +82,7 @@ export interface TypeScope {
exportedTypes: Record<string, ScopeTypeNode>
}

export interface WithScope {
export interface MaybeWithScope {
_ownerScope?: TypeScope
}

Expand All @@ -100,7 +103,7 @@ interface ResolvedElements {
*/
export function resolveTypeElements(
ctx: TypeResolveContext,
node: Node & WithScope & { _resolvedElements?: ResolvedElements },
node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
scope?: TypeScope
): ResolvedElements {
if (node._resolvedElements) {
Expand Down Expand Up @@ -177,7 +180,7 @@ function typeElementsToMap(
const res: ResolvedElements = { props: {} }
for (const e of elements) {
if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
;(e as WithScope)._ownerScope = scope
;(e as MaybeWithScope)._ownerScope = scope
const name = getId(e.key)
if (name && !e.computed) {
res.props[name] = e as ResolvedElements['props'][string]
Expand Down Expand Up @@ -248,7 +251,7 @@ function createProperty(

function resolveInterfaceMembers(
ctx: TypeResolveContext,
node: TSInterfaceDeclaration & WithScope,
node: TSInterfaceDeclaration & MaybeWithScope,
scope: TypeScope
): ResolvedElements {
const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
Expand Down Expand Up @@ -289,7 +292,7 @@ function resolveIndexType(
ctx: TypeResolveContext,
node: TSIndexedAccessType,
scope: TypeScope
): (TSType & WithScope)[] {
): (TSType & MaybeWithScope)[] {
if (node.indexType.type === 'TSNumberKeyword') {
return resolveArrayElementType(ctx, node.objectType, scope)
}
Expand All @@ -308,7 +311,7 @@ function resolveIndexType(
for (const key of keys) {
const targetType = resolved.props[key]?.typeAnnotation?.typeAnnotation
if (targetType) {
;(targetType as TSType & WithScope)._ownerScope =
;(targetType as TSType & MaybeWithScope)._ownerScope =
resolved.props[key]._ownerScope
types.push(targetType)
}
Expand Down Expand Up @@ -532,22 +535,22 @@ function innerResolveTypeReference(
}
}
} else {
const ns = innerResolveTypeReference(
ctx,
scope,
name[0],
node,
onlyExported
)
if (ns && ns.type === 'TSModuleDeclaration') {
const childScope = moduleDeclToScope(ns, scope)
return innerResolveTypeReference(
ctx,
childScope,
name.length > 2 ? name.slice(1) : name[name.length - 1],
node,
!ns.declare
)
let ns = innerResolveTypeReference(ctx, scope, name[0], node, onlyExported)
if (ns) {
if (ns.type !== 'TSModuleDeclaration') {
// namespace merged with other types, attached as _ns
ns = ns._ns
}
if (ns) {
const childScope = moduleDeclToScope(ns, ns._ownerScope || scope)
return innerResolveTypeReference(
ctx,
childScope,
name.length > 2 ? name.slice(1) : name[name.length - 1],
node,
!ns.declare
)
}
}
}
}
Expand Down Expand Up @@ -771,7 +774,6 @@ export function fileToScope(
exportedTypes: Object.create(null)
}
recordTypes(body, scope, asGlobal)

fileToScopeCache.set(filename, scope)
return scope
}
Expand Down Expand Up @@ -858,10 +860,21 @@ function moduleDeclToScope(
}
const scope: TypeScope = {
...parentScope,
imports: Object.create(parentScope.imports),
// TODO this seems wrong
types: Object.create(parentScope.types),
imports: Object.create(parentScope.imports)
exportedTypes: Object.create(null)
}

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)
}
recordTypes((node.body as TSModuleBlock).body, scope)

return (node._resolvedChildScope = scope)
}

Expand Down Expand Up @@ -923,20 +936,52 @@ function recordTypes(body: Statement[], scope: TypeScope, asGlobal = false) {
}
}
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
}
}

function recordType(node: Node, types: Record<string, Node>) {
switch (node.type) {
case 'TSInterfaceDeclaration':
case 'TSEnumDeclaration':
case 'TSModuleDeclaration':
case 'ClassDeclaration': {
const id = node.id.type === 'Identifier' ? node.id.name : node.id.value
types[id] = node
case 'TSModuleDeclaration': {
const id = getId(node.id)
let existing = types[id]
if (existing) {
if (node.type === 'TSModuleDeclaration') {
if (existing.type === 'TSModuleDeclaration') {
mergeNamespaces(existing as typeof node, node)
} else {
attachNamespace(existing, node)
}
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':
types[node.id.name] = node.typeAnnotation
break
Expand All @@ -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[]) {
const imports: TypeScope['imports'] = Object.create(null)
for (const s of body) {
Expand All @@ -977,7 +1063,7 @@ function recordImport(node: Node, imports: TypeScope['imports']) {

export function inferRuntimeType(
ctx: TypeResolveContext,
node: Node & WithScope,
node: Node & MaybeWithScope,
scope = node._ownerScope || ctxToScope(ctx)
): string[] {
switch (node.type) {
Expand Down Expand Up @@ -1035,11 +1121,11 @@ export function inferRuntimeType(
}

case 'TSTypeReference':
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
}
if (node.typeName.type === 'Identifier') {
const resolved = resolveTypeReference(ctx, node, scope)
if (resolved) {
return inferRuntimeType(ctx, resolved, resolved._ownerScope)
}
switch (node.typeName.name) {
case 'Array':
case 'Function':
Expand Down

0 comments on commit d53e157

Please sign in to comment.