Skip to content

Commit

Permalink
feat: collect errors (#412)
Browse files Browse the repository at this point in the history
Resolves first part of #410 
Laying the groudwork and collect evaluation errors.

---

Creates errors like below. Note that the formatting is not final. We should probably do some tree based output.
The errors can also be inspected programatically.

```
DeclarativeStackError:

[RuntimeError at Resources.SiteDistribution]
    Cannot read properties of undefined (reading 'bind')

[TypeError at Resources.SiteDistribution.Properties.defaultBehavior.origin."aws-cdk-lib.aws_cloudfront_origins.S3Origin"]
    Cannot read properties of undefined (reading 'isWebsite')

[RuntimeError at Resources.SiteDistribution.Properties.defaultBehavior.origin."aws-cdk-lib.aws_cloudfront_origins.S3Origin".0.Ref]
    No such Resource or Parameter: SiteCertificate

[RuntimeError at Resources.SiteDistribution.Properties.defaultBehavior.origin."aws-cdk-lib.aws_cloudfront_origins.S3Origin".1.originAccessIdentity.Ref]
    No such Resource or Parameter: CloudFrontOAI

[RuntimeError at Resources.SiteDistribution.Properties.certificate.Ref]
    No such Resource or Parameter: SiteBucket

[RuntimeError at Resources.SiteDistribution.Properties.domainNames.0.Ref]
    No such Resource or Parameter: DomainName
```
  • Loading branch information
mrgrain authored Oct 26, 2022
1 parent e5cc482 commit afd2592
Show file tree
Hide file tree
Showing 18 changed files with 579 additions and 287 deletions.
13 changes: 13 additions & 0 deletions src/decdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,26 @@ import * as cdk from 'aws-cdk-lib';
import chalk from 'chalk';
import yargs from 'yargs';
import { DeclarativeStack } from './declarative-stack';
import { DeclarativeStackError } from './error-handling';
import { loadTypeSystem, readTemplate, stackNameFromFileName } from './util';

let verbosity = 0;

async function main() {
const argv = await yargs
.usage(
'$0 <filename>',
'Synthesize a CDK stack from a declarative JSON or YAML template'
)
.option('verbose', {
alias: 'v',
type: 'count',
description: 'Show debug output. Repeat to increase verbosity.',
})
.positional('filename', { type: 'string', required: true }).argv;

verbosity = argv.verbose;

const templateFile = argv.filename;
if (!templateFile) {
throw new Error('filename is missing');
Expand All @@ -35,6 +45,9 @@ async function main() {
}

main().catch((e) => {
if (e instanceof DeclarativeStackError) {
e = e.toString(verbosity >= 1);
}
// eslint-disable-next-line no-console
console.error(chalk.red(e));
process.exit(1);
Expand Down
8 changes: 7 additions & 1 deletion src/declarative-stack.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as reflect from 'jsii-reflect';
import { AnnotationsContext, DeclarativeStackError } from './error-handling';
import { EvaluationContext, Evaluator } from './evaluate';
import { Template } from './parser/template';
import { TypedTemplate } from './type-resolution/template';
Expand Down Expand Up @@ -37,6 +38,7 @@ export class DeclarativeStack extends cdk.Stack {
};
})(this, '$decdkAnalytics');

const annotations = AnnotationsContext.root();
const context = new EvaluationContext({
stack: this,
template,
Expand All @@ -45,8 +47,12 @@ export class DeclarativeStack extends cdk.Stack {
const ev = new Evaluator(context);

_cwd(props.workingDirectory, () => {
ev.evaluateTemplate();
ev.evaluateTemplate(annotations);
});

if (annotations.hasErrors()) {
throw new DeclarativeStackError(annotations);
}
}
}

Expand Down
56 changes: 56 additions & 0 deletions src/error-handling/annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { AnnotatedError } from './errors';

export class AnnotationsContext {
public static root(): AnnotationsContext {
return new AnnotationsContext();
}

public readonly children = new Array<AnnotationsContext>();
public readonly path: Array<string | number> = [];
public readonly parent?: AnnotationsContext;
private readonly _errorsStack: AnnotatedError[] = [];

private constructor(parent?: AnnotationsContext, path?: string | number) {
if (parent) {
this.path.push(...parent.path);
this.parent = parent;
parent.children.push(this);
}

if (path !== undefined) {
this.path.push(path);
}
}

public get root(): boolean {
return !!this.parent;
}

public hasErrors(): boolean {
return this.errors.length > 0;
}

public get errors(): AnnotatedError[] {
return [...this._errorsStack, ...this.children.flatMap((c) => c.errors)];
}

public child(path: string | number): AnnotationsContext {
return new AnnotationsContext(this, path);
}

public wrap<T>(fn: (ctx: AnnotationsContext) => T): T | void {
try {
return fn(this);
} catch (error) {
this.error(error as any);
}
}

public error(error: Error) {
this._errorsStack.push(new AnnotatedError(this.path, error));
}

public toString(printStackStrace = false) {
return this.errors.map((e) => e.toString(printStackStrace)).join('\n\n');
}
}
68 changes: 68 additions & 0 deletions src/error-handling/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { AnnotationsContext } from './annotations';

function indent(text: string, spaces = 4) {
const TAB = ' '.repeat(spaces);
return text
.split('\n')
.map((l) => TAB + l)
.join('\n');
}

export class DeclarativeStackError extends Error {
constructor(public readonly annotations: AnnotationsContext) {
super();
this.name = 'DeclarativeStackError';
this.message = this.toString();
Object.setPrototypeOf(this, DeclarativeStackError.prototype);
}

public toString(debug = false) {
return `${this.name}:\n\n${this.annotations.toString(debug)}`;
}
}

/**
* Thrown by the Evaluator
*/
export class RuntimeError extends Error {
public static wrap(error: any) {
return new RuntimeError(error.message, error.stack);
}

constructor(message: string, stack?: string) {
super(message);
this.name = 'RuntimeError';
if (stack) {
this.stack = stack;
}
Object.setPrototypeOf(this, RuntimeError.prototype);
}
}

/**
* Annotation any Error with additional info
*/
export class AnnotatedError {
constructor(
public readonly stack: Array<string | number>,
public readonly error: Error
) {}

public toString(printStackStrace = false) {
const details = this.renderErrorDetails(printStackStrace);
return `[${this.error.name} at ${this.renderStack()}]\n${indent(details)}`;
}

protected renderErrorDetails(printStackStrace = false): string {
if (printStackStrace) {
return this.error.stack ?? this.error.message;
}
return this.error.message;
}

protected renderStack(): string {
return this.stack
.map((s) => (s.toString().includes('.') ? `"${s}"` : s))
.join('.');
}
}
3 changes: 3 additions & 0 deletions src/error-handling/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './annotations';
export * from './errors';
export * from './unparse';
28 changes: 28 additions & 0 deletions src/error-handling/unparse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
INTRINSIC_NAME_MAP,
TemplateExpression,
UserIntrinsicExpression,
} from '../parser/template';

export function unparseExpression(x: TemplateExpression): any {
switch (x.type) {
case 'string':
case 'number':
case 'boolean':
return x.value;
case 'null':
return null;
case 'array':
return x.array.map(unparseExpression);
case 'object':
return Object.fromEntries(
Object.entries(x.fields).map(([k, v]) => [k, unparseExpression(v)])
);
default:
return x;
}
}

export function intrinsicToLongForm(fn: UserIntrinsicExpression['fn']): string {
return INTRINSIC_NAME_MAP?.[fn] ?? fn;
}
9 changes: 5 additions & 4 deletions src/evaluate/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as cdk from 'aws-cdk-lib';
import * as reflect from 'jsii-reflect';
import { RuntimeError } from '../error-handling';
import { TypedTemplate } from '../type-resolution/template';
import { InstanceReference, Reference, References } from './references';

Expand Down Expand Up @@ -47,23 +48,23 @@ export class EvaluationContext {
public reference(logicalId: string): Reference {
const r = this.availableRefs.get(logicalId);
if (!r) {
throw new Error(`No resource or parameter with name: ${logicalId}`);
throw new RuntimeError(`No such Resource or Parameter: ${logicalId}`);
}
return r;
}

public mapping(mappingName: string) {
const map = this.template?.mappings?.get(mappingName);
if (!map) {
throw new Error(`No such Mapping: ${mappingName}`);
throw new RuntimeError(`No such Mapping: ${mappingName}`);
}
return map;
}

public condition(conditionName: string) {
const condition = this.template?.conditions?.get(conditionName);
if (!condition) {
throw new Error(`No such condition: ${conditionName}`);
throw new RuntimeError(`No such condition: ${conditionName}`);
}
return condition;
}
Expand All @@ -84,7 +85,7 @@ export class EvaluationContext {
}
curr = curr[next];
if (!curr) {
throw new Error(`unable to resolve class ${className}`);
throw new RuntimeError(`Class not found: ${fqn}`);
}
}

Expand Down
Loading

0 comments on commit afd2592

Please sign in to comment.