From f4c8dcdadf8de1ab832fe33d0fab4cdd05b9ab64 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 18 Jun 2019 11:01:30 +0300 Subject: [PATCH] feat(core): show token creation stack trace upon resolve error (#2886) Allows implementers of IResolvable to supply a stack trace, which will automatically be attached to errors thrown during resolution. Implement this for lazy and intrinsic tokens. --- packages/@aws-cdk/cdk/lib/lazy.ts | 8 ++++- .../@aws-cdk/cdk/lib/private/intrinsic.ts | 6 ++-- packages/@aws-cdk/cdk/lib/resolvable.ts | 32 +++++++++++++------ packages/@aws-cdk/cdk/test/test.tokens.ts | 19 ++++++++++- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/cdk/lib/lazy.ts b/packages/@aws-cdk/cdk/lib/lazy.ts index bcf1d59a10b12..5c74cde9a5b9e 100644 --- a/packages/@aws-cdk/cdk/lib/lazy.ts +++ b/packages/@aws-cdk/cdk/lib/lazy.ts @@ -1,3 +1,4 @@ +import { createStackTrace } from './private/stack-trace'; import { IResolvable, IResolveContext } from "./resolvable"; import { Token } from "./token"; @@ -119,8 +120,13 @@ export class Lazy { } abstract class LazyBase implements IResolvable { - public abstract resolve(context: IResolveContext): any; + public readonly creationStack: string[]; + + constructor() { + this.creationStack = createStackTrace(); + } + public abstract resolve(context: IResolveContext): any; public toString() { return Token.asString(this); } diff --git a/packages/@aws-cdk/cdk/lib/private/intrinsic.ts b/packages/@aws-cdk/cdk/lib/private/intrinsic.ts index f7469cf4f542c..0c9c3c0990c25 100644 --- a/packages/@aws-cdk/cdk/lib/private/intrinsic.ts +++ b/packages/@aws-cdk/cdk/lib/private/intrinsic.ts @@ -16,7 +16,7 @@ export class Intrinsic implements IResolvable { /** * The captured stack trace which represents the location in which this token was created. */ - protected readonly trace: string[]; + public readonly creationStack: string[]; private readonly value: any; @@ -25,7 +25,7 @@ export class Intrinsic implements IResolvable { throw new Error(`Argument to Intrinsic must be a plain value object, got ${value}`); } - this.trace = createStackTrace(); + this.creationStack = createStackTrace(); this.value = value; } @@ -68,7 +68,7 @@ export class Intrinsic implements IResolvable { * @param message Error message */ protected newError(message: string): any { - return new Error(`${message}\nToken created:\n at ${this.trace.join('\n at ')}\nError thrown:`); + return new Error(`${message}\nToken created:\n at ${this.creationStack.join('\n at ')}\nError thrown:`); } } diff --git a/packages/@aws-cdk/cdk/lib/resolvable.ts b/packages/@aws-cdk/cdk/lib/resolvable.ts index ba7d830573781..b3ecf1327c386 100644 --- a/packages/@aws-cdk/cdk/lib/resolvable.ts +++ b/packages/@aws-cdk/cdk/lib/resolvable.ts @@ -29,6 +29,12 @@ export interface IResolveContext { * Tokens are special objects that participate in synthesis. */ export interface IResolvable { + /** + * The creation stack of this resolvable which will be appended to errors + * thrown during resolution. + */ + readonly creationStack?: string[]; + /** * Produce the Token's value at resolution time */ @@ -113,14 +119,22 @@ export class DefaultTokenResolver implements ITokenResolver { * then finally post-process it. */ public resolveToken(t: IResolvable, context: IResolveContext, postProcessor: IPostProcessor) { - let resolved = t.resolve(context); - - // The token might have returned more values that need resolving, recurse - resolved = context.resolve(resolved); - - resolved = postProcessor.postProcess(resolved, context); - - return resolved; + try { + let resolved = t.resolve(context); + + // The token might have returned more values that need resolving, recurse + resolved = context.resolve(resolved); + resolved = postProcessor.postProcess(resolved, context); + return resolved; + } catch (e) { + let message = `Resolution error: ${e.message}.`; + if (t.creationStack && t.creationStack.length > 0) { + message += `\nObject creation stack:\n at ${t.creationStack.join('\n at ')}`; + } + + e.message = message; + throw e; + } } /** @@ -145,4 +159,4 @@ export class DefaultTokenResolver implements ITokenResolver { return fragments.mapTokens({ mapToken: context.resolve }).firstValue; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cdk/test/test.tokens.ts b/packages/@aws-cdk/cdk/test/test.tokens.ts index 10afd2932e5d1..9ad3ad18dcd25 100644 --- a/packages/@aws-cdk/cdk/test/test.tokens.ts +++ b/packages/@aws-cdk/cdk/test/test.tokens.ts @@ -477,7 +477,7 @@ export = { function fn2() { class ExposeTrace extends Intrinsic { public get creationTrace() { - return this.trace; + return this.creationStack; } } @@ -582,6 +582,23 @@ export = { return tests; })(), + + 'creation stack is attached to errors emitted during resolve'(test: Test) { + function showMeInTheStackTrace() { + return Lazy.stringValue({ produce: () => { throw new Error('fooError'); } }); + } + + const x = showMeInTheStackTrace(); + let message; + try { + resolve(x); + } catch (e) { + message = e.message; + } + + test.ok(message && message.includes('showMeInTheStackTrace')); + test.done(); + } }; class Promise2 implements IResolvable {