Skip to content

Commit

Permalink
feat: change error handling to a continuation function approach
Browse files Browse the repository at this point in the history
  • Loading branch information
wessberg committed Jul 29, 2022
1 parent b67b873 commit 8f7cfdb
Show file tree
Hide file tree
Showing 112 changed files with 1,355 additions and 694 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
"@wessberg/prettier-config": "^1.0.0",
"rollup-plugin-ts": "3.0.2",
"ava": "^3.15.0",
"crosspath": "^2.0.0",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
Expand Down Expand Up @@ -90,7 +89,8 @@
},
"dependencies": {
"ansi-colors": "^4.1.3",
"object-path": "^0.11.8"
"object-path": "^0.11.8",
"crosspath": "^2.0.0"
},
"peerDependencies": {
"typescript": ">=3.2.x || >= 4.x",
Expand Down
15 changes: 8 additions & 7 deletions src/interpreter/environment/create-sanitized-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,40 @@ import {ProcessError} from "../error/policy-error/process-error/process-error.js
import {isProcessSpawnChildOperation} from "../policy/process/is-process-spawn-child-operation.js";
import {ICreateSanitizedEnvironmentOptions} from "./i-create-sanitized-environment-options.js";
import {isConsoleOperation} from "../policy/console/is-console-operation.js";
import { EvaluationErrorIntent } from "../error/evaluation-error/evaluation-error-intent.js";

/**
* Creates an environment that provide hooks into policy checks
*/
export function createSanitizedEnvironment({policy, env, getCurrentNode}: ICreateSanitizedEnvironmentOptions): IndexLiteral {
export function createSanitizedEnvironment({policy, env}: ICreateSanitizedEnvironmentOptions): IndexLiteral {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hook = (item: PolicyProxyHookOptions<any>) => {
if (!policy.console && isConsoleOperation(item)) {
return false;
}

if (!policy.io.read && isIoRead(item)) {
throw new IoError({kind: "read", node: getCurrentNode()});
return new EvaluationErrorIntent((node, options) => new IoError({...options, node, kind: "read"}));
}

if (!policy.io.write && isIoWrite(item)) {
throw new IoError({kind: "write", node: getCurrentNode()});
return new EvaluationErrorIntent((node, options) => new IoError({...options, node, kind: "write"}));
}

if (!policy.process.exit && isProcessExitOperation(item)) {
throw new ProcessError({kind: "exit", node: getCurrentNode()});
return new EvaluationErrorIntent((node, options) => new ProcessError({...options, node, kind: "exit"}));
}

if (!policy.process.exit && isProcessSpawnChildOperation(item)) {
throw new ProcessError({kind: "spawnChild", node: getCurrentNode()});
return new EvaluationErrorIntent((node, options) => new ProcessError({...options, node, kind: "spawnChild"}));
}

if (!policy.network && isNetworkOperation(item)) {
throw new NetworkError({operation: stringifyPolicyTrapKindOnPath(item.kind, item.path), node: getCurrentNode()});
return new EvaluationErrorIntent((node, options) => new NetworkError({...options, node, operation: stringifyPolicyTrapKindOnPath(item.kind, item.path)}));
}

if (policy.deterministic && isNonDeterministic(item)) {
throw new NonDeterministicError({operation: stringifyPolicyTrapKindOnPath(item.kind, item.path), node: getCurrentNode()});
return new EvaluationErrorIntent((node, options) => new NonDeterministicError({...options, node, operation: stringifyPolicyTrapKindOnPath(item.kind, item.path)}));
}

return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {EvaluatePolicySanitized} from "../policy/evaluate-policy.js";
import {IndexLiteral} from "../literal/literal.js";
import {TS} from "../../type/ts.js";

export interface ICreateSanitizedEnvironmentOptions {
policy: EvaluatePolicySanitized;
env: IndexLiteral;
getCurrentNode(): TS.Node;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {IAsyncIteratorNotSupportedErrorOptions} from "./i-async-iterator-not-sup
* An Error that can be thrown when an async iteration operation is attempted
*/
export class AsyncIteratorNotSupportedError extends EvaluationError {
constructor({message = `It is not possible to evaluate an async iterator'`, typescript}: IAsyncIteratorNotSupportedErrorOptions) {
super({message, node: typescript.createEmptyStatement()});
constructor({message = `It is not possible to evaluate an async iterator'`, typescript, environment}: IAsyncIteratorNotSupportedErrorOptions) {
super({message, environment, node: typescript.factory?.createEmptyStatement() ?? typescript.createEmptyStatement()});
}
}
20 changes: 20 additions & 0 deletions src/interpreter/error/evaluation-error/evaluation-error-intent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {TS} from "../../../type/ts.js";
import {NextEvaluatorOptions} from "../../evaluator/evaluator-options.js";
import {EvaluationError} from "./evaluation-error.js";

type EvaluationErrorIntentCallback<T extends EvaluationError> = (node: TS.Node, options: NextEvaluatorOptions) => T;

export class EvaluationErrorIntent<T extends EvaluationError = EvaluationError> {
constructor(private readonly intent: EvaluationErrorIntentCallback<T>) {}
construct(node: TS.Node, options: NextEvaluatorOptions): T {
return this.intent(node, options);
}
}

export function isEvaluationErrorIntent<T extends EvaluationError = EvaluationError>(item: unknown): item is EvaluationErrorIntent<T> {
return typeof item === "object" && item != null && item instanceof EvaluationErrorIntent;
}

export function maybeThrow<Value>(node: TS.Node, options: NextEvaluatorOptions, value: Value | EvaluationErrorIntent): Value | EvaluationError {
return isEvaluationErrorIntent(value) ? options.throwError(value.construct(node, options)) : value;
}
11 changes: 10 additions & 1 deletion src/interpreter/error/evaluation-error/evaluation-error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {IEvaluationErrorOptions} from "./i-evaluation-error-options.js";
import {TS} from "../../../type/ts.js";
import { LexicalEnvironment } from "../../lexical-environment/lexical-environment.js";

export type ThrowError = (error: EvaluationError) => EvaluationError;

/**
* A Base class for EvaluationErrors
Expand All @@ -9,10 +12,16 @@ export class EvaluationError extends Error {
* The node that caused or thew the error
*/
readonly node: TS.Node;
readonly environment: LexicalEnvironment;

constructor({node, message}: IEvaluationErrorOptions) {
constructor({node, environment, message}: IEvaluationErrorOptions) {
super(message);
Error.captureStackTrace(this, this.constructor);
this.node = node;
this.environment = environment;
}
}

export function isEvaluationError (item: unknown): item is EvaluationError {
return typeof item === "object" && item != null && item instanceof EvaluationError;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {TS} from "../../../type/ts.js";
import { LexicalEnvironment } from "../../lexical-environment/lexical-environment.js";

export interface IEvaluationErrorOptions {
node: TS.Node;
environment: LexicalEnvironment;
message?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class MissingCatchOrFinallyAfterTryError extends EvaluationError {
*/
readonly node!: TS.TryStatement;

constructor({node, message = `Missing catch or finally after try`}: IMissingCatchOrFinallyAfterTryErrorOptions) {
super({node, message});
constructor({node, environment, message = `Missing catch or finally after try`}: IMissingCatchOrFinallyAfterTryErrorOptions) {
super({node, environment, message});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class ModuleNotFoundError extends EvaluationError {
*/
readonly path: string;

constructor({path, node, message = `Module '${path}' could not be resolved'`}: IModuleNotFoundErrorOptions) {
super({message, node});
constructor({path, node, environment, message = `Module '${path}' could not be resolved'`}: IModuleNotFoundErrorOptions) {
super({message, environment, node});
this.path = path;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class NotCallableError extends EvaluationError {
*/
readonly value: Literal;

constructor({value, node, message = `${stringifyLiteral(value)} is not a function'`}: INotCallableErrorOptions) {
super({message, node});
constructor({value, node, environment, message = `${stringifyLiteral(value)} is not a function'`}: INotCallableErrorOptions) {
super({message, environment, node});
this.value = value;
}
}
4 changes: 2 additions & 2 deletions src/interpreter/error/policy-error/io-error/io-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class IoError extends PolicyError {
*/
readonly kind: keyof EvaluateIOPolicy;

constructor({node, kind, message = `${kind} operations are in violation of the policy`}: IIoErrorOptions) {
super({violation: "io", message, node});
constructor({node, environment, kind, message = `${kind} operations are in violation of the policy`}: IIoErrorOptions) {
super({violation: "io", message, environment, node});
this.kind = kind;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class MaxOpDurationExceededError extends PolicyError {
*/
readonly duration: number;

constructor({duration, node, message = `Maximum operation duration exceeded: ${duration}`}: IMaxOpDurationExceededErrorOptions) {
super({violation: "maxOpDuration", message, node});
constructor({duration, environment, node, message = `Maximum operation duration exceeded: ${duration}`}: IMaxOpDurationExceededErrorOptions) {
super({violation: "maxOpDuration", message, node, environment});
this.duration = duration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class MaxOpsExceededError extends PolicyError {
*/
readonly ops: number;

constructor({ops, node, message = `Maximum ops exceeded: ${ops}`}: IMaxOpsExceededErrorOptions) {
super({violation: "maxOps", message, node});
constructor({ops, node, environment, message = `Maximum ops exceeded: ${ops}`}: IMaxOpsExceededErrorOptions) {
super({violation: "maxOps", message, node, environment});
this.ops = ops;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class NetworkError extends PolicyError {
*/
readonly operation: string;

constructor({operation, node, message = `The operation: '${operation}' is performing network activity. That is in violation of the policy`}: INetworkErrorOptions) {
super({violation: "deterministic", message, node});
constructor({operation, node, environment, message = `The operation: '${operation}' is performing network activity. That is in violation of the policy`}: INetworkErrorOptions) {
super({violation: "deterministic", message, node, environment});

this.operation = operation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class NonDeterministicError extends PolicyError {
*/
readonly operation: string;

constructor({operation, node, message = `The operation: '${operation}' is nondeterministic. That is in violation of the policy`}: INonDeterministicErrorOptions) {
super({violation: "deterministic", message, node});
constructor({operation, node, environment, message = `The operation: '${operation}' is nondeterministic. That is in violation of the policy`}: INonDeterministicErrorOptions) {
super({violation: "deterministic", message, node, environment});

this.operation = operation;
}
Expand Down
4 changes: 2 additions & 2 deletions src/interpreter/error/policy-error/policy-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class PolicyError extends EvaluationError {
*/
readonly violation: keyof EvaluatePolicySanitized;

constructor({violation, node, message}: IPolicyErrorOptions) {
super({node, message: `[${violation}]: ${message}`});
constructor({violation, node, environment, message}: IPolicyErrorOptions) {
super({node, environment, message: `[${violation}]: ${message}`});
this.violation = violation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export class ProcessError extends PolicyError {
*/
readonly kind: keyof EvaluateProcessPolicy;

constructor({kind, node, message = `${kind} operations are in violation of the policy`}: IProcessErrorOptions) {
super({violation: "process", message, node});
constructor({kind, node, environment, message = `${kind} operations are in violation of the policy`}: IProcessErrorOptions) {
super({violation: "process", message, node, environment});
this.kind = kind;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class UndefinedIdentifierError extends EvaluationError {
*/
readonly node!: TS.Identifier | TS.PrivateIdentifier;

constructor({node, message = `'${node.text}' is not defined'`}: IUndefinedIdentifierErrorOptions) {
super({message, node});
constructor({node, environment, message = `'${node.text}' is not defined'`}: IUndefinedIdentifierErrorOptions) {
super({message, environment, node});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {IUndefinedLeftValueErrorOptions} from "./i-undefined-left-value-error-op
* An Error that can be thrown when an undefined leftValue is encountered
*/
export class UndefinedLeftValueError extends EvaluationError {
constructor({node, message = `'No leftValue could be determined'`}: IUndefinedLeftValueErrorOptions) {
super({message, node});
constructor({node, environment, message = `'No leftValue could be determined'`}: IUndefinedLeftValueErrorOptions) {
super({message, environment, node});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {IUnexpectedNodeErrorOptions} from "./i-unexpected-node-error-options.js"
* An Error that can be thrown when an unexpected node is encountered
*/
export class UnexpectedNodeError extends EvaluationError {
constructor({node, typescript, message = `Unexpected Node: '${typescript.SyntaxKind[node.kind]}'`}: IUnexpectedNodeErrorOptions) {
super({message, node});
constructor({node, environment, typescript, message = `Unexpected Node: '${typescript.SyntaxKind[node.kind]}'`}: IUnexpectedNodeErrorOptions) {
super({message, node, environment});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {IUnexpectedSyntaxErrorOptions} from "./i-unexpected-syntax-error-options
* An Error that can be thrown when a certain usage is to be considered a SyntaxError
*/
export class UnexpectedSyntaxError extends EvaluationError {
constructor({node, message = `'SyntaxError'`}: IUnexpectedSyntaxErrorOptions) {
super({message, node});
constructor({node, environment, message = `'SyntaxError'`}: IUnexpectedSyntaxErrorOptions) {
super({message, environment, node});
}
}
Loading

0 comments on commit 8f7cfdb

Please sign in to comment.