refactor: improve type resolve error output

This commit is contained in:
Evan You 2023-04-13 21:36:23 +08:00
parent 8aa4ea81d6
commit c93c11710e
3 changed files with 101 additions and 50 deletions

View File

@ -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, {

View File

@ -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
)}` )}`
) )
} }

View File

@ -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)