mirror of https://github.com/vuejs/core.git
refactor: improve type resolve error output
This commit is contained in:
parent
8aa4ea81d6
commit
c93c11710e
|
@ -327,16 +327,34 @@ describe('resolveType', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('errors', () => {
|
describe('errors', () => {
|
||||||
test('error on computed keys', () => {
|
test('failed type reference', () => {
|
||||||
expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
|
expect(() => resolve(`type Target = X`)).toThrow(
|
||||||
`computed keys are not supported in types referenced by SFC macros`
|
`Unresolvable type reference`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('unsupported computed keys', () => {
|
||||||
|
expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
|
||||||
|
`Unsupported computed key in type referenced by a macro`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unsupported index type', () => {
|
||||||
|
expect(() => resolve(`type Target = X[K]`)).toThrow(
|
||||||
|
`Unsupported index type`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('failed improt source resolve', () => {
|
||||||
|
expect(() =>
|
||||||
|
resolve(`import { X } from './foo'; type Target = X`)
|
||||||
|
).toThrow(`Failed to resolve import source "./foo" for type X`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function resolve(code: string, files: Record<string, string> = {}) {
|
function resolve(code: string, files: Record<string, string> = {}) {
|
||||||
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`, {
|
const { descriptor } = parse(`<script setup lang="ts">\n${code}\n</script>`, {
|
||||||
filename: 'Test.vue'
|
filename: 'Test.vue'
|
||||||
})
|
})
|
||||||
const ctx = new ScriptCompileContext(descriptor, {
|
const ctx = new ScriptCompileContext(descriptor, {
|
||||||
|
|
|
@ -126,13 +126,14 @@ export class ScriptCompileContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
error(msg: string, node: Node & WithScope, scope?: TypeScope): never {
|
error(msg: string, node: Node & WithScope, scope?: TypeScope): never {
|
||||||
|
const offset = scope ? scope.offset || 0 : this.startOffset!
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[@vue/compiler-sfc] ${msg}\n\n${
|
`[@vue/compiler-sfc] ${msg}\n\n${
|
||||||
this.descriptor.filename
|
(scope || this.descriptor).filename
|
||||||
}\n${generateCodeFrame(
|
}\n${generateCodeFrame(
|
||||||
this.descriptor.source,
|
(scope || this.descriptor).source,
|
||||||
node.start! + this.startOffset!,
|
node.start! + offset,
|
||||||
node.end! + this.startOffset!
|
node.end! + offset
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ type Import = Pick<ImportBinding, 'source' | 'imported'>
|
||||||
export interface TypeScope {
|
export interface TypeScope {
|
||||||
filename: string
|
filename: string
|
||||||
source: string
|
source: string
|
||||||
|
offset: number
|
||||||
imports: Record<string, Import>
|
imports: Record<string, Import>
|
||||||
types: Record<
|
types: Record<
|
||||||
string,
|
string,
|
||||||
|
@ -128,7 +129,8 @@ function innerResolveTypeElements(
|
||||||
} else {
|
} else {
|
||||||
ctx.error(
|
ctx.error(
|
||||||
`Unsupported index type: ${node.indexType.type}`,
|
`Unsupported index type: ${node.indexType.type}`,
|
||||||
node.indexType
|
node.indexType,
|
||||||
|
scope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,16 +146,17 @@ function innerResolveTypeElements(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
SupportedBuiltinsSet.has(typeName)
|
SupportedBuiltinsSet.has(typeName)
|
||||||
) {
|
) {
|
||||||
return resolveBuiltin(ctx, node, typeName as any)
|
return resolveBuiltin(ctx, node, typeName as any, scope)
|
||||||
}
|
}
|
||||||
ctx.error(
|
ctx.error(
|
||||||
`Failed to resolve type reference, or unsupported built-in utlility type.`,
|
`Unresolvable type reference or unsupported built-in utlility type`,
|
||||||
node
|
node,
|
||||||
|
scope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.error(`Unresolvable type in SFC macro: ${node.type}`, node)
|
ctx.error(`Unresolvable type: ${node.type}`, node, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
function typeElementsToMap(
|
function typeElementsToMap(
|
||||||
|
@ -169,13 +172,14 @@ function typeElementsToMap(
|
||||||
if (name && !e.computed) {
|
if (name && !e.computed) {
|
||||||
res.props[name] = e as ResolvedElements['props'][string]
|
res.props[name] = e as ResolvedElements['props'][string]
|
||||||
} 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, scope)) {
|
||||||
res.props[key] = e as ResolvedElements['props'][string]
|
res.props[key] = e as ResolvedElements['props'][string]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.error(
|
ctx.error(
|
||||||
`computed keys are not supported in types referenced by SFC macros.`,
|
`Unsupported computed key in type referenced by a macro`,
|
||||||
e
|
e.key,
|
||||||
|
scope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (e.type === 'TSCallSignatureDeclaration') {
|
} else if (e.type === 'TSCallSignatureDeclaration') {
|
||||||
|
@ -256,10 +260,7 @@ function resolveMappedType(
|
||||||
scope: TypeScope
|
scope: TypeScope
|
||||||
): ResolvedElements {
|
): ResolvedElements {
|
||||||
const res: ResolvedElements = { props: {} }
|
const res: ResolvedElements = { props: {} }
|
||||||
if (!node.typeParameter.constraint) {
|
const keys = resolveStringType(ctx, node.typeParameter.constraint!, scope)
|
||||||
ctx.error(`mapped type used in macros must have a finite constraint.`, node)
|
|
||||||
}
|
|
||||||
const keys = resolveStringType(ctx, node.typeParameter.constraint)
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
res.props[key] = createProperty(
|
res.props[key] = createProperty(
|
||||||
{
|
{
|
||||||
|
@ -273,25 +274,29 @@ function resolveMappedType(
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] {
|
function resolveStringType(
|
||||||
|
ctx: ScriptCompileContext,
|
||||||
|
node: Node,
|
||||||
|
scope: TypeScope
|
||||||
|
): string[] {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'StringLiteral':
|
case 'StringLiteral':
|
||||||
return [node.value]
|
return [node.value]
|
||||||
case 'TSLiteralType':
|
case 'TSLiteralType':
|
||||||
return resolveStringType(ctx, node.literal)
|
return resolveStringType(ctx, node.literal, scope)
|
||||||
case 'TSUnionType':
|
case 'TSUnionType':
|
||||||
return node.types.map(t => resolveStringType(ctx, t)).flat()
|
return node.types.map(t => resolveStringType(ctx, t, scope)).flat()
|
||||||
case 'TemplateLiteral': {
|
case 'TemplateLiteral': {
|
||||||
return resolveTemplateKeys(ctx, node)
|
return resolveTemplateKeys(ctx, node, scope)
|
||||||
}
|
}
|
||||||
case 'TSTypeReference': {
|
case 'TSTypeReference': {
|
||||||
const resolved = resolveTypeReference(ctx, node)
|
const resolved = resolveTypeReference(ctx, node, scope)
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
return resolveStringType(ctx, resolved)
|
return resolveStringType(ctx, resolved, scope)
|
||||||
}
|
}
|
||||||
if (node.typeName.type === 'Identifier') {
|
if (node.typeName.type === 'Identifier') {
|
||||||
const getParam = (index = 0) =>
|
const getParam = (index = 0) =>
|
||||||
resolveStringType(ctx, node.typeParameters!.params[index])
|
resolveStringType(ctx, node.typeParameters!.params[index], scope)
|
||||||
switch (node.typeName.name) {
|
switch (node.typeName.name) {
|
||||||
case 'Extract':
|
case 'Extract':
|
||||||
return getParam(1)
|
return getParam(1)
|
||||||
|
@ -308,17 +313,18 @@ function resolveStringType(ctx: ScriptCompileContext, node: Node): string[] {
|
||||||
case 'Uncapitalize':
|
case 'Uncapitalize':
|
||||||
return getParam().map(s => s[0].toLowerCase() + s.slice(1))
|
return getParam().map(s => s[0].toLowerCase() + s.slice(1))
|
||||||
default:
|
default:
|
||||||
ctx.error('Failed to resolve type reference', node)
|
ctx.error('Failed to resolve type reference', node, scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.error('Failed to resolve string type into finite keys', node)
|
ctx.error('Failed to resolve string type into finite keys', node, scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTemplateKeys(
|
function resolveTemplateKeys(
|
||||||
ctx: ScriptCompileContext,
|
ctx: ScriptCompileContext,
|
||||||
node: TemplateLiteral
|
node: TemplateLiteral,
|
||||||
|
scope: TypeScope
|
||||||
): string[] {
|
): string[] {
|
||||||
if (!node.expressions.length) {
|
if (!node.expressions.length) {
|
||||||
return [node.quasis[0].value.raw]
|
return [node.quasis[0].value.raw]
|
||||||
|
@ -328,12 +334,16 @@ function resolveTemplateKeys(
|
||||||
const e = node.expressions[0]
|
const e = node.expressions[0]
|
||||||
const q = node.quasis[0]
|
const q = node.quasis[0]
|
||||||
const leading = q ? q.value.raw : ``
|
const leading = q ? q.value.raw : ``
|
||||||
const resolved = resolveStringType(ctx, e)
|
const resolved = resolveStringType(ctx, e, scope)
|
||||||
const restResolved = resolveTemplateKeys(ctx, {
|
const restResolved = resolveTemplateKeys(
|
||||||
...node,
|
ctx,
|
||||||
expressions: node.expressions.slice(1),
|
{
|
||||||
quasis: q ? node.quasis.slice(1) : node.quasis
|
...node,
|
||||||
})
|
expressions: node.expressions.slice(1),
|
||||||
|
quasis: q ? node.quasis.slice(1) : node.quasis
|
||||||
|
},
|
||||||
|
scope
|
||||||
|
)
|
||||||
|
|
||||||
for (const r of resolved) {
|
for (const r of resolved) {
|
||||||
for (const rr of restResolved) {
|
for (const rr of restResolved) {
|
||||||
|
@ -357,7 +367,8 @@ type GetSetType<T> = T extends Set<infer V> ? V : never
|
||||||
function resolveBuiltin(
|
function resolveBuiltin(
|
||||||
ctx: ScriptCompileContext,
|
ctx: ScriptCompileContext,
|
||||||
node: TSTypeReference | TSExpressionWithTypeArguments,
|
node: TSTypeReference | TSExpressionWithTypeArguments,
|
||||||
name: GetSetType<typeof SupportedBuiltinsSet>
|
name: GetSetType<typeof SupportedBuiltinsSet>,
|
||||||
|
scope: TypeScope
|
||||||
): ResolvedElements {
|
): ResolvedElements {
|
||||||
const t = resolveTypeElements(ctx, node.typeParameters!.params[0])
|
const t = resolveTypeElements(ctx, node.typeParameters!.params[0])
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
@ -366,7 +377,11 @@ function resolveBuiltin(
|
||||||
case 'Readonly':
|
case 'Readonly':
|
||||||
return t
|
return t
|
||||||
case 'Pick': {
|
case 'Pick': {
|
||||||
const picked = resolveStringType(ctx, node.typeParameters!.params[1])
|
const picked = resolveStringType(
|
||||||
|
ctx,
|
||||||
|
node.typeParameters!.params[1],
|
||||||
|
scope
|
||||||
|
)
|
||||||
const res: ResolvedElements = { props: {}, calls: t.calls }
|
const res: ResolvedElements = { props: {}, calls: t.calls }
|
||||||
for (const key of picked) {
|
for (const key of picked) {
|
||||||
res.props[key] = t.props[key]
|
res.props[key] = t.props[key]
|
||||||
|
@ -374,7 +389,11 @@ function resolveBuiltin(
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
case 'Omit':
|
case 'Omit':
|
||||||
const omitted = resolveStringType(ctx, node.typeParameters!.params[1])
|
const omitted = resolveStringType(
|
||||||
|
ctx,
|
||||||
|
node.typeParameters!.params[1],
|
||||||
|
scope
|
||||||
|
)
|
||||||
const res: ResolvedElements = { props: {}, calls: t.calls }
|
const res: ResolvedElements = { props: {}, calls: t.calls }
|
||||||
for (const key in t.props) {
|
for (const key in t.props) {
|
||||||
if (!omitted.includes(key)) {
|
if (!omitted.includes(key)) {
|
||||||
|
@ -415,7 +434,7 @@ function innerResolveTypeReference(
|
||||||
): Node | undefined {
|
): Node | undefined {
|
||||||
if (typeof name === 'string') {
|
if (typeof name === 'string') {
|
||||||
if (scope.imports[name]) {
|
if (scope.imports[name]) {
|
||||||
return resolveTypeFromImport(ctx, scope, scope.imports[name], node)
|
return resolveTypeFromImport(ctx, node, name, scope)
|
||||||
} else {
|
} else {
|
||||||
const types = onlyExported ? scope.exportedTypes : scope.types
|
const types = onlyExported ? scope.exportedTypes : scope.types
|
||||||
return types[name]
|
return types[name]
|
||||||
|
@ -462,19 +481,21 @@ function qualifiedNameToPath(node: Identifier | TSQualifiedName): string[] {
|
||||||
|
|
||||||
function resolveTypeFromImport(
|
function resolveTypeFromImport(
|
||||||
ctx: ScriptCompileContext,
|
ctx: ScriptCompileContext,
|
||||||
scope: TypeScope,
|
node: TSTypeReference | TSExpressionWithTypeArguments,
|
||||||
{ source, imported }: Import,
|
name: string,
|
||||||
node: TSTypeReference | TSExpressionWithTypeArguments
|
scope: TypeScope
|
||||||
): Node | undefined {
|
): Node | undefined {
|
||||||
const fs = ctx.options.fs
|
const fs = ctx.options.fs
|
||||||
if (!fs) {
|
if (!fs) {
|
||||||
ctx.error(
|
ctx.error(
|
||||||
`fs options for compileScript are required for resolving imported types`,
|
`fs options for compileScript are required for resolving imported types`,
|
||||||
node
|
node,
|
||||||
|
scope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// TODO (hmr) register dependency file on ctx
|
// TODO (hmr) register dependency file on ctx
|
||||||
const containingFile = scope.filename
|
const containingFile = scope.filename
|
||||||
|
const { source, imported } = scope.imports[name]
|
||||||
if (source.startsWith('.')) {
|
if (source.startsWith('.')) {
|
||||||
// relative import - fast path
|
// relative import - fast path
|
||||||
const filename = path.join(containingFile, '..', source)
|
const filename = path.join(containingFile, '..', source)
|
||||||
|
@ -488,7 +509,13 @@ function resolveTypeFromImport(
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ctx.error(`Failed to resolve import source for type`, node)
|
ctx.error(
|
||||||
|
`Failed to resolve import source ${JSON.stringify(
|
||||||
|
source
|
||||||
|
)} for type ${name}`,
|
||||||
|
node,
|
||||||
|
scope
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO module or aliased import - use full TS resolution
|
// TODO module or aliased import - use full TS resolution
|
||||||
|
@ -519,10 +546,11 @@ function fileToScope(
|
||||||
): TypeScope {
|
): TypeScope {
|
||||||
// TODO cache
|
// TODO cache
|
||||||
const source = fs.readFile(filename)
|
const source = fs.readFile(filename)
|
||||||
const body = parseFile(ctx, filename, source)
|
const [body, offset] = parseFile(ctx, filename, source)
|
||||||
const scope: TypeScope = {
|
const scope: TypeScope = {
|
||||||
filename,
|
filename,
|
||||||
source,
|
source,
|
||||||
|
offset,
|
||||||
types: Object.create(null),
|
types: Object.create(null),
|
||||||
exportedTypes: Object.create(null),
|
exportedTypes: Object.create(null),
|
||||||
imports: recordImports(body)
|
imports: recordImports(body)
|
||||||
|
@ -535,10 +563,12 @@ function parseFile(
|
||||||
ctx: ScriptCompileContext,
|
ctx: ScriptCompileContext,
|
||||||
filename: string,
|
filename: string,
|
||||||
content: string
|
content: string
|
||||||
): Statement[] {
|
): [Statement[], number] {
|
||||||
|
let body: Statement[] = []
|
||||||
|
let offset = 0
|
||||||
const ext = path.extname(filename)
|
const ext = path.extname(filename)
|
||||||
if (ext === '.ts' || ext === '.tsx') {
|
if (ext === '.ts' || ext === '.tsx') {
|
||||||
return babelParse(content, {
|
body = babelParse(content, {
|
||||||
plugins: resolveParserPlugins(
|
plugins: resolveParserPlugins(
|
||||||
ext.slice(1),
|
ext.slice(1),
|
||||||
ctx.options.babelParserPlugins
|
ctx.options.babelParserPlugins
|
||||||
|
@ -551,12 +581,13 @@ function parseFile(
|
||||||
} = parse(content)
|
} = parse(content)
|
||||||
const scriptContent = (script?.content || '') + (scriptSetup?.content || '')
|
const scriptContent = (script?.content || '') + (scriptSetup?.content || '')
|
||||||
const lang = script?.lang || scriptSetup?.lang
|
const lang = script?.lang || scriptSetup?.lang
|
||||||
return babelParse(scriptContent, {
|
body = babelParse(scriptContent, {
|
||||||
plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins),
|
plugins: resolveParserPlugins(lang!, ctx.options.babelParserPlugins),
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
}).program.body
|
}).program.body
|
||||||
|
offset = scriptSetup ? scriptSetup.loc.start.offset : 0
|
||||||
}
|
}
|
||||||
return []
|
return [body, offset]
|
||||||
}
|
}
|
||||||
|
|
||||||
function ctxToScope(ctx: ScriptCompileContext): TypeScope {
|
function ctxToScope(ctx: ScriptCompileContext): TypeScope {
|
||||||
|
@ -567,6 +598,7 @@ function ctxToScope(ctx: ScriptCompileContext): TypeScope {
|
||||||
const scope: TypeScope = {
|
const scope: TypeScope = {
|
||||||
filename: ctx.descriptor.filename,
|
filename: ctx.descriptor.filename,
|
||||||
source: ctx.descriptor.source,
|
source: ctx.descriptor.source,
|
||||||
|
offset: ctx.startOffset!,
|
||||||
imports: Object.create(ctx.userImports),
|
imports: Object.create(ctx.userImports),
|
||||||
types: Object.create(null),
|
types: Object.create(null),
|
||||||
exportedTypes: Object.create(null)
|
exportedTypes: Object.create(null)
|
||||||
|
|
Loading…
Reference in New Issue