Skip to content

Commit

Permalink
feat(reporting): adds a new reporter: reportErrors. This is a reporti…
Browse files Browse the repository at this point in the history
…ng hook that will be invoked for each error that is thrown, both when evaluating a result, and for subsequent invocations on, for example, returned function instances. Holds a reference to the error, as well ast the AST node that threw or caused the Error
  • Loading branch information
wessberg committed Dec 30, 2018
1 parent 4bed17e commit a65e386
Show file tree
Hide file tree
Showing 16 changed files with 124 additions and 56 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -165,7 +165,8 @@ const result = evaluate({
reporting: {
reportBindings: entry => doSomething(entry),
reportTraversal: entry => someArray.push(entry.node),
reportIntermediateResults: entry => doSomeOtherThing(entry)
reportIntermediateResults: entry => doSomeOtherThing(entry),
reportErrors: entry => doSomethingWithError(entry)
}
});
```
Expand All @@ -175,6 +176,7 @@ Here's an explainer of the different reporting hooks:
- `reportBindings(entry: IBindingReportEntry) => void|(Promise<void>)` - Will be invoked for each time a value is bound to the lexical environment of a Node. This is useful to track mutations throughout code execution, for example to understand when and where variables are declared and/or mutated.
- `reportTraversal(entry: ITraversalReportEntry) => void|(Promise<void>)` - Will be invoked for each time a new Node is visited while evaluating. This is useful to track the path through the AST, for example to compute code coverage.
- `reportIntermediateResults(entry: IIntermediateResultReportEntry) => void|(Promise<void>)` - Will be invoked for each intermediate result that has been evaluated before producing a final result. This allows you to work programmatically with all expression values during code execution.
- `reportErrors(entry: IErrorReportEntry) => void|(Promise<void>)` - Will be invoked for each error that is thrown, both when evaluating a result, and for subsequent invocations on, for example, returned function instances. Holds a reference to the error, as well ast the AST node that threw or caused the Error.

## Contributing

Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Expand Up @@ -14,7 +14,6 @@ export {EvaluationError} from "./interpreter/error/evaluation-error/evaluation-e
export {MissingCatchOrFinallyAfterTryError} from "./interpreter/error/missing-catch-or-finally-after-try-error/missing-catch-or-finally-after-try-error";
export {ModuleNotFoundError} from "./interpreter/error/module-not-found-error/module-not-found-error";
export {NotCallableError} from "./interpreter/error/not-callable-error/not-callable-error";
export {ThrownError} from "./interpreter/error/thrown-error/thrown-error";
export {PolicyError} from "./interpreter/error/policy-error/policy-error";
export {UndefinedIdentifierError} from "./interpreter/error/undefined-identifier-error/undefined-identifier-error";
export {UndefinedLeftValueError} from "./interpreter/error/undefined-left-value-error/undefined-left-value-error";
Expand Down
5 changes: 0 additions & 5 deletions src/interpreter/error/thrown-error/i-thrown-error-options.ts

This file was deleted.

18 changes: 0 additions & 18 deletions src/interpreter/error/thrown-error/thrown-error.ts

This file was deleted.

22 changes: 14 additions & 8 deletions src/interpreter/evaluate.ts
Expand Up @@ -15,8 +15,9 @@ import {UnexpectedNodeError} from "./error/unexpected-node-error/unexpected-node
import {IEvaluatePolicySanitized} from "./policy/i-evaluate-policy";
import {EnvironmentPresetKind} from "./environment/environment-preset-kind";
import {Node} from "typescript";
import {EvaluationError} from "./error/evaluation-error/evaluation-error";
import {ThrownError} from "./error/thrown-error/thrown-error";
import {reportError} from "./util/reporting/report-error";
import {createReportedErrorSet} from "./reporting/reported-error-set";
import {ReportingOptionsSanitized} from "./reporting/i-reporting-options";

/**
* Will get a literal value for the given Expression, ExpressionStatement, or Declaration.
Expand Down Expand Up @@ -46,7 +47,7 @@ export function evaluate ({
spawnChild: false
}
} = {},
reporting = {}
reporting: reportingInput = {}
}: IEvaluateOptions): EvaluateResult {
// Take the simple path first. This may be far more performant than building up an environment
const simpleLiteralResult = evaluateSimpleLiteral(node);
Expand All @@ -70,6 +71,12 @@ export function evaluate ({
}
};

// Sanitize the Reporting options based on the input options
const reporting: ReportingOptionsSanitized = {
...reportingInput,
reportedErrorSet: createReportedErrorSet()
};

// Prepare a reference to the Node that is currently being evaluated
let currentNode: Node = node;

Expand All @@ -95,7 +102,7 @@ export function evaluate ({
typeChecker,
logger,
stack,
reporting,
reporting: reporting,
nextNode: nextNode => currentNode = nextNode
});

Expand Down Expand Up @@ -129,10 +136,9 @@ export function evaluate ({
value
};
} catch (reason) {
// If the Error hasn't been wrapped or wasn't thrown internally, wrap it in a ThrownError
if (!(reason instanceof EvaluationError)) {
reason = new ThrownError({originalError: reason, node: currentNode});
}
// Report the Error
reportError(reporting, reason, node);

return {
success: false,
reason
Expand Down
7 changes: 6 additions & 1 deletion src/interpreter/evaluator/evaluate-try-statement.ts
@@ -1,24 +1,29 @@
import {IEvaluatorOptions} from "./i-evaluator-options";
import {TryStatement} from "typescript";
import {MissingCatchOrFinallyAfterTryError} from "../error/missing-catch-or-finally-after-try-error/missing-catch-or-finally-after-try-error";
import {clearBindingFromLexicalEnvironment, setInLexicalEnvironment} from "../lexical-environment/lexical-environment";
import {TRY_SYMBOL} from "../util/try/try-symbol";

/**
* Evaluates, or attempts to evaluate, a TryStatement
* @param {IEvaluatorOptions<TryStatement>} options
* @returns {Promise<void>}
*/
export function evaluateTryStatement ({node, evaluate, environment, statementTraversalStack}: IEvaluatorOptions<TryStatement>): void {
export function evaluateTryStatement ({node, evaluate, environment, reporting, statementTraversalStack}: IEvaluatorOptions<TryStatement>): void {
const executeTry = () => {
setInLexicalEnvironment({env: environment, reporting, newBinding: true, node, path: TRY_SYMBOL, value: true});
// The Block will declare an environment of its own
evaluate.statement(node.tryBlock, environment);
};

const executeCatch = (ex: Error) => {
clearBindingFromLexicalEnvironment(environment, TRY_SYMBOL);
// The CatchClause will declare an environment of its own
evaluate.nodeWithArgument(node.catchClause!, environment, ex, statementTraversalStack);
};

const executeFinally = () => {
clearBindingFromLexicalEnvironment(environment, TRY_SYMBOL);
// The Block will declare an environment of its own
evaluate.statement(node.finallyBlock!, environment);
};
Expand Down
4 changes: 2 additions & 2 deletions src/interpreter/evaluator/i-evaluator-options.ts
Expand Up @@ -5,15 +5,15 @@ import {Logger} from "../logger/logger";
import {StatementTraversalStack} from "../stack/traversal-stack/statement-traversal-stack";
import {Stack} from "../stack/stack";
import {IEvaluatePolicySanitized} from "../policy/i-evaluate-policy";
import {ReportingOptions} from "../reporting/i-reporting-options";
import {ReportingOptionsSanitized} from "../reporting/i-reporting-options";

export interface IEvaluatorOptions<T extends (Node|NodeArray<Node>)> {
node: T;
typeChecker: TypeChecker;
evaluate: NodeEvaluator;
environment: LexicalEnvironment;
policy: IEvaluatePolicySanitized;
reporting: ReportingOptions;
reporting: ReportingOptionsSanitized;
stack: Stack;
statementTraversalStack: StatementTraversalStack;
logger: Logger;
Expand Down
59 changes: 47 additions & 12 deletions src/interpreter/evaluator/node-evaluator/create-node-evaluator.ts
Expand Up @@ -2,7 +2,7 @@ import {Declaration, Expression, Node, Statement} from "typescript";
import {ICreateNodeEvaluatorOptions} from "./i-create-node-evaluator-options";
import {NodeEvaluator, NodeWithValue} from "./node-evaluator";
import {MaxOpsExceededError} from "../../error/policy-error/max-ops-exceeded-error/max-ops-exceeded-error";
import {LexicalEnvironment} from "../../lexical-environment/lexical-environment";
import {LexicalEnvironment, pathInLexicalEnvironmentEquals} from "../../lexical-environment/lexical-environment";
import {evaluateStatement} from "../evaluate-statement";
import {Literal} from "../../literal/literal";
import {evaluateExpression} from "../evaluate-expression";
Expand All @@ -11,6 +11,8 @@ import {evaluateDeclaration} from "../evaluate-declaration";
import {evaluateNodeWithArgument} from "../evaluate-node-with-argument";
import {evaluateNodeWithValue} from "../evaluate-node-with-value";
import {createStatementTraversalStack, StatementTraversalStack} from "../../stack/traversal-stack/statement-traversal-stack";
import {reportError} from "../../util/reporting/report-error";
import {TRY_SYMBOL} from "../../util/try/try-symbol";

/**
* Creates a Node Evaluator
Expand Down Expand Up @@ -38,27 +40,60 @@ export function createNodeEvaluator ({typeChecker, policy, logger, stack, report
}
};

/**
* Wraps an evaluation action with error reporting
* @param {LexicalEnvironment} environment
* @param {ts.Node} node
* @param {Function} action
*/
const wrapWithErrorReporting = (environment: LexicalEnvironment, node: Node, action: Function) => {
// If we're already inside of a try-block, simply execute the action and do nothing else
if (pathInLexicalEnvironmentEquals(node, environment, true, TRY_SYMBOL)) {
return action();
}

try {
return action();
} catch (ex) {
// Report the Error
reportError(reporting, ex, node);

// Re-throw the error
throw ex;
}
};

const nodeEvaluator: NodeEvaluator = {
expression: (node: Expression, environment: LexicalEnvironment, statementTraversalStack: StatementTraversalStack): Literal => {
handleNewNode(node, statementTraversalStack);
return evaluateExpression(getEvaluatorOptions(node, environment, statementTraversalStack));
return wrapWithErrorReporting(environment, node, () => {
handleNewNode(node, statementTraversalStack);
return evaluateExpression(getEvaluatorOptions(node, environment, statementTraversalStack));
});
},
statement: (node: Statement, environment: LexicalEnvironment): void => {
const statementTraversalStack = createStatementTraversalStack();
handleNewNode(node, statementTraversalStack);
return evaluateStatement(getEvaluatorOptions(node, environment, statementTraversalStack));
return wrapWithErrorReporting(environment, node, () => {
const statementTraversalStack = createStatementTraversalStack();
handleNewNode(node, statementTraversalStack);
return evaluateStatement(getEvaluatorOptions(node, environment, statementTraversalStack));
});
},
declaration: (node: Declaration, environment: LexicalEnvironment, statementTraversalStack: StatementTraversalStack): void => {
handleNewNode(node, statementTraversalStack);
return evaluateDeclaration(getEvaluatorOptions(node, environment, statementTraversalStack));
return wrapWithErrorReporting(environment, node, () => {
handleNewNode(node, statementTraversalStack);
return evaluateDeclaration(getEvaluatorOptions(node, environment, statementTraversalStack));
});
},
nodeWithArgument: (node: Node, environment: LexicalEnvironment, arg: Literal, statementTraversalStack: StatementTraversalStack): void => {
handleNewNode(node, statementTraversalStack);
return evaluateNodeWithArgument(getEvaluatorOptions(node, environment, statementTraversalStack), arg);
return wrapWithErrorReporting(environment, node, () => {
handleNewNode(node, statementTraversalStack);
return evaluateNodeWithArgument(getEvaluatorOptions(node, environment, statementTraversalStack), arg);
});
},
nodeWithValue: (node: NodeWithValue, environment: LexicalEnvironment, statementTraversalStack: StatementTraversalStack): Literal => {
handleNewNode(node, statementTraversalStack);
return evaluateNodeWithValue(getEvaluatorOptions(node, environment, statementTraversalStack));
return wrapWithErrorReporting(environment, node, () => {
handleNewNode(node, statementTraversalStack);
return evaluateNodeWithValue(getEvaluatorOptions(node, environment, statementTraversalStack));
});
}
};

Expand Down
@@ -1,13 +1,13 @@
import {TypeChecker, Node} from "typescript";
import {Node, TypeChecker} from "typescript";
import {Logger} from "../../logger/logger";
import {Stack} from "../../stack/stack";
import {IEvaluatePolicySanitized} from "../../policy/i-evaluate-policy";
import {ReportingOptions} from "../../reporting/i-reporting-options";
import {ReportingOptionsSanitized} from "../../reporting/i-reporting-options";

export interface ICreateNodeEvaluatorOptions {
typeChecker: TypeChecker;
policy: IEvaluatePolicySanitized;
reporting: ReportingOptions;
reporting: ReportingOptionsSanitized;
logger: Logger;
stack: Stack;
nextNode (node: Node): void;
Expand Down
@@ -1,13 +1,13 @@
import {Node} from "typescript";
import {Literal} from "../literal/literal";
import {LexicalEnvironment} from "./lexical-environment";
import {ReportingOptions} from "../reporting/i-reporting-options";
import {ReportingOptionsSanitized} from "../reporting/i-reporting-options";

export interface ISetInLexicalEnvironmentOptions {
env: LexicalEnvironment;
path: string;
value: Literal;
reporting: ReportingOptions;
reporting: ReportingOptionsSanitized;
node: Node;
newBinding?: boolean;
}
2 changes: 1 addition & 1 deletion src/interpreter/lexical-environment/lexical-environment.ts
Expand Up @@ -157,7 +157,7 @@ export function setInLexicalEnvironment ({env, path, value, reporting, node, new
export function clearBindingFromLexicalEnvironment (env: LexicalEnvironment, path: string): void {
const [firstBinding] = path.split(".");
if (has(env.env, firstBinding)) {
del(firstBinding);
del(env.env, path);
}

else {
Expand Down
14 changes: 13 additions & 1 deletion src/interpreter/reporting/i-reporting-options.ts
@@ -1,4 +1,5 @@
import {Expression, Node} from "typescript";
import {ReportedErrorSet} from "./reported-error-set";

export interface IBindingReportEntry {
path: string;
Expand All @@ -15,14 +16,25 @@ export interface IIntermediateResultReportEntry {
value: unknown;
}

export interface IErrorReportEntry {
node: Node;
error: Error;
}

export type BindingReportCallback = (entry: IBindingReportEntry) => void|(Promise<void>);
export type ErrorReportCallback = (entry: IErrorReportEntry) => void|(Promise<void>);
export type IntermediateResultReportCallback = (entry: IIntermediateResultReportEntry) => void|(Promise<void>);
export type TraversalReportCallback = (entry: ITraversalReportEntry) => void|(Promise<void>);

export interface IReportingOptions {
reportBindings: BindingReportCallback;
reportTraversal: TraversalReportCallback;
reportIntermediateResults: IntermediateResultReportCallback;
reportErrors: ErrorReportCallback;
}

export type ReportingOptions = Partial<IReportingOptions>;
export type ReportingOptions = Partial<IReportingOptions>;

export interface ReportingOptionsSanitized extends ReportingOptions {
reportedErrorSet: ReportedErrorSet;
}
9 changes: 9 additions & 0 deletions src/interpreter/reporting/reported-error-set.ts
@@ -0,0 +1,9 @@
export type ReportedErrorSet = WeakSet<Error>;

/**
* Creates and returns a Set of Errors that has been seen and has been reported
* @returns {WeakSet<Error>}
*/
export function createReportedErrorSet (): ReportedErrorSet {
return new WeakSet<Error>();
}
22 changes: 22 additions & 0 deletions src/interpreter/util/reporting/report-error.ts
@@ -0,0 +1,22 @@
import {Node} from "typescript";
import {ReportingOptionsSanitized} from "../../reporting/i-reporting-options";
import {EvaluationError} from "../../error/evaluation-error/evaluation-error";

/**
* Reports an error
* @param {ReportingOptionsSanitized} reporting
* @param {Error} error
* @param {Node} node
*/
export function reportError (reporting: ReportingOptionsSanitized, error: Error, node: Node): void {
// Report the error if a reporter is hooked up
if (reporting.reportErrors != null && !reporting.reportedErrorSet.has(error)) {
reporting.reportedErrorSet.add(error);
reporting.reportErrors({
error: error,
node: error instanceof EvaluationError
? error.node
: node
});
}
}
1 change: 1 addition & 0 deletions src/interpreter/util/try/try-symbol.ts
@@ -0,0 +1 @@
export const TRY_SYMBOL = "[try]";
2 changes: 1 addition & 1 deletion test/type-of-expression/type-of-expression.test.ts
Expand Up @@ -22,7 +22,7 @@ test("Can evaluate a TypeOfExpression #1", t => {
test("Can evaluate a TypeOfExpression #2", t => {
const {evaluate} = prepareTest(
// language=TypeScript
`
`
(() => {
let a = BigInt(2);
if (typeof a === "bigint") return "foo";
Expand Down

0 comments on commit a65e386

Please sign in to comment.