From 4e31cd8b016367388f811ca8bb0583b4a8513fed Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 7 May 2019 18:46:55 +0200 Subject: [PATCH] fix(assembler): handle unknown types without crashing After failing compilation we proceed with jsii analysis, to give as much information as possible in one go. However, some compilation failures lead to a lack of information that make the jsii analyzer crash, which ultimately leads jsii to fail with no more information than: Error: TypeError: Cannot read property 'getJsDocTags' of undefined Fix this in two ways: - First, detect the situation where this occurs and produce a more useful error message. - Guard against this happening in general by catching exceptions during type analysis and still printing the compilation errors. --- packages/jsii/lib/assembler.ts | 24 ++++++++++++++++++++++-- packages/jsii/lib/compiler.ts | 32 ++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/jsii/lib/assembler.ts b/packages/jsii/lib/assembler.ts index 7ddbf1e5b8..bf06ad98f3 100644 --- a/packages/jsii/lib/assembler.ts +++ b/packages/jsii/lib/assembler.ts @@ -377,7 +377,6 @@ export class Assembler implements Emitter { const processBaseTypes = (types: ts.Type[]) => { for (const iface of types) { - // base is private/internal, so we continue recursively with it's own bases if (this._isPrivateOrInternal(iface.symbol)) { erasedBases.push(iface); @@ -507,7 +506,7 @@ export class Assembler implements Emitter { // process all "implements" clauses const allInterfaces = new Set(); for (const clause of implementsClauses) { - const { interfaces } = await this._processBaseInterfaces(fqn, clause.types.map(t => this._typeChecker.getTypeFromTypeNode(t))); + const { interfaces } = await this._processBaseInterfaces(fqn, clause.types.map(t => this._getTypeFromTypeNode(t))); for (const ifc of (interfaces || [])) { allInterfaces.add(ifc.fqn); } @@ -623,6 +622,17 @@ export class Assembler implements Emitter { return _sortMembers(jsiiType); } + /** + * Use the TypeChecker's getTypeFromTypeNode, but throw a descriptive error if it fails + */ + private _getTypeFromTypeNode(t: ts.TypeNode) { + const type = this._typeChecker.getTypeFromTypeNode(t); + if (isErrorType(type)) { + throw new Error(`Unable to resolve type: ${t.getFullText()}. This typically happens if something is wrong with your dependency closure.`); + } + return type; + } + /** * Check that this class doesn't declare any members that are of different staticness in itself or any of its bases */ @@ -1585,3 +1595,13 @@ function noEmptyDict(xs: {[key: string]: T}): {[key: string]: T} | undefined if (Object.keys(xs).length === 0) { return undefined; } return xs; } + +/** + * Check whether this type is the intrinsic TypeScript "error type" + * + * This type is returned if type lookup fails. Unfortunately no public + * accessors for it are exposed. + */ +function isErrorType(t: ts.Type) { + return (t as any).intrinsicName === 'error'; +} \ No newline at end of file diff --git a/packages/jsii/lib/compiler.ts b/packages/jsii/lib/compiler.ts index e808dec9d9..d437094d90 100644 --- a/packages/jsii/lib/compiler.ts +++ b/packages/jsii/lib/compiler.ts @@ -141,22 +141,30 @@ export class Compiler implements Emitter { private async _consumeProgram(program: ts.Program, stdlib: string): Promise { const emit = program.emit(); - if (emit.emitSkipped) { + let hasErrors = emitHasErrors(emit); + const diagnostics = [...emit.diagnostics]; + + if (hasErrors) { LOG.error('Compilation errors prevented the JSII assembly from being created'); } // we continue to do jsii checker even if there are compilation errors so that - // jsii warnings will appear. - const assembler = new Assembler(this.options.projectInfo, program, stdlib); - const assmEmit = await assembler.emit(); - if (assmEmit.hasErrors) { - LOG.error('Type model errors prevented the JSII assembly from being created'); + // jsii warnings will appear. However, the Assembler might throw an exception + // because broken/missing type information might lead it to fail completely. + try { + const assembler = new Assembler(this.options.projectInfo, program, stdlib); + const assmEmit = await assembler.emit(); + if (assmEmit.hasErrors) { + LOG.error('Type model errors prevented the JSII assembly from being created'); + } + + hasErrors = hasErrors || assmEmit.hasErrors; + diagnostics.push(...assmEmit.diagnostics); + } catch (e) { + LOG.error(`Error during type model analysis: ${e}`); } - return { - hasErrors: emit.emitSkipped || assmEmit.hasErrors, - diagnostics: [...emit.diagnostics, ...assmEmit.diagnostics] - }; + return { hasErrors, diagnostics }; } /** @@ -353,3 +361,7 @@ function parseConfigHostFromCompilerHost(host: ts.CompilerHost): ts.ParseConfigH trace: host.trace ? (s) => host.trace!(s) : undefined }; } + +function emitHasErrors(result: ts.EmitResult) { + return result.diagnostics.some(d => d.category === ts.DiagnosticCategory.Error) || result.emitSkipped; +}