mirror of https://github.com/vuejs/core.git
fix(compiler-sfc): handle type merging + fix namespace access when inferring type
close #8102
This commit is contained in:
parent
5510ce385a
commit
d53e157805
|
@ -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', () => {
|
||||||
|
|
|
@ -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':
|
||||||
|
|
Loading…
Reference in New Issue