Skip to content

Commit

Permalink
feat(errors): Throw ExecutionException instead of Error when poss…
Browse files Browse the repository at this point in the history
…ible
  • Loading branch information
jubnzv committed Sep 7, 2024
1 parent c8d3839 commit c8eb532
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 44 deletions.
17 changes: 8 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Runner, MistiResult } from "./driver";
import { ExecutionException } from "./internals/exceptions";
import { MISTI_VERSION, TACT_VERSION } from "./version";
import { Command } from "commander";
import { createDetector } from "./createDetector";
Expand Down Expand Up @@ -74,7 +75,7 @@ export function createMistiCommand(): Command {
.split(",")
.filter((detector) => detector.trim() !== "");
if (detectors.length === 0) {
throw new Error(
throw ExecutionException.make(
"The --detectors option requires a non-empty list of detector names.",
);
}
Expand Down Expand Up @@ -102,15 +103,13 @@ export function createMistiCommand(): Command {
}

if (!PROJECT_CONFIG_OR_FILE_PATH) {
throw new Error("`<TACT_CONFIG_PATH|TACT_FILE_PATH>` is required");
throw ExecutionException.make(
"`<TACT_CONFIG_PATH|TACT_FILE_PATH>` is required",
);
}

try {
RUNNER = await Runner.make(PROJECT_CONFIG_OR_FILE_PATH, options);
await RUNNER.run();
} catch (error) {
throw new Error(`An error occurred: ${error}`);
}
RUNNER = await Runner.make(PROJECT_CONFIG_OR_FILE_PATH, options);
await RUNNER.run();
});

return command;
Expand All @@ -126,7 +125,7 @@ export async function runMistiCommand(
const command = createMistiCommand();
if (args.length === 0) {
command.help();
throw new Error("No arguments provided. Help displayed.");
throw ExecutionException.make("No arguments provided. Help displayed.");
} else {
await command.parseAsync(args, { from: "user" });
return RUNNER === undefined ? undefined : RUNNER!.getResult();
Expand Down
5 changes: 4 additions & 1 deletion src/detectors/builtin/divideBeforeMultiply.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SouffleDetector } from "../detector";
import { CompilationUnit, BasicBlock, CFG } from "../../internals/ir";
import { MistiTactWarning, Severity } from "../../internals/warnings";
import { InternalException } from "../../internals/exceptions";
import {
forEachExpression,
forEachStatement,
Expand Down Expand Up @@ -51,7 +52,9 @@ export class DivideBeforeMultiply extends SouffleDetector {
this.addConstraints(cu, program);
return await this.executeSouffle(program, (fact) => {
if (fact.data === undefined) {
throw new Error(`AST position for fact ${fact} is not available`);
throw InternalException.make(
`AST position for fact ${fact} is not available`,
);
}
return this.makeWarning(
"Divide Before Multiply",
Expand Down
5 changes: 4 additions & 1 deletion src/detectors/builtin/readOnlyVariables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SouffleDetector, WarningsBehavior } from "../detector";
import { CompilationUnit, BasicBlock, CFG } from "../../internals/ir";
import { MistiTactWarning, Severity } from "../../internals/warnings";
import { InternalException } from "../../internals/exceptions";
import { extractPath, forEachExpression } from "../../internals/tactASTUtil";
import { SouffleContext, relation, rule, body, atom } from "@nowarp/souffle";
import {
Expand Down Expand Up @@ -49,7 +50,9 @@ export class ReadOnlyVariables extends SouffleDetector {
this.addConstraints(cu, program);
return await this.executeSouffle(program, (fact) => {
if (fact.data === undefined) {
throw new Error(`AST position for fact ${fact} is not available`);
throw InternalException.make(
`AST position for fact ${fact} is not available`,
);
}
if (this.skipUnused(fact.data.contents)) {
return undefined;
Expand Down
5 changes: 4 additions & 1 deletion src/detectors/builtin/unboundLoops.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SouffleDetector } from "../detector";
import { CompilationUnit, BasicBlock, CFG } from "../../internals/ir";
import { InternalException } from "../../internals/exceptions";
import { MistiTactWarning, Severity } from "../../internals/warnings";
import {
extractPath,
Expand Down Expand Up @@ -50,7 +51,9 @@ export class UnboundLoops extends SouffleDetector {
this.addConstraints(cu, program);
return await this.executeSouffle(program, (fact) => {
if (fact.data === undefined) {
throw new Error(`AST position for fact ${fact} is not available`);
throw InternalException.make(
`AST position for fact ${fact} is not available`,
);
}
return this.makeWarning("Unbounded Loop", Severity.MEDIUM, fact.data, {
suggestion:
Expand Down
3 changes: 2 additions & 1 deletion src/detectors/detector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MistiContext } from "../internals/context";
import { CompilationUnit } from "../internals/ir";
import { InternalException } from "../internals/exceptions";
import { makeDocURL, MistiTactWarning, Severity } from "../internals/warnings";
import {
SouffleContext,
Expand Down Expand Up @@ -128,7 +129,7 @@ export abstract class SouffleDetector extends Detector {
});
const result = await executor.execute(ctx);
if (result.kind !== "structured") {
throw new Error(
throw InternalException.make(
`Error executing Soufflé for ${this.id}:\n${result.kind === "error" ? result.stderr : "Cannot unmarshal raw output:\n" + result.results}`,
);
}
Expand Down
30 changes: 20 additions & 10 deletions src/driver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { MistiContext } from "./internals/context";
import { Logger } from "./internals/logger";
import { tryMsg, InternalException } from "./internals/exceptions";
import {
tryMsg,
InternalException,
ExecutionException,
} from "./internals/exceptions";
import { createIR } from "./internals/ir/builders/tactIRBuilder";
import { GraphvizDumper, JSONDumper } from "./internals/irDump";
import { ProjectName, CompilationUnit } from "./internals/ir";
Expand Down Expand Up @@ -58,7 +62,7 @@ export class Driver {
const singleContract = tactPath.endsWith(".tact");
// Check if the input file exists.
if (!fs.existsSync(tactPath)) {
throw new Error(
throw ExecutionException.make(
`${singleContract ? "Contract" : "Project"} ${tactPath} is not available.`,
);
}
Expand Down Expand Up @@ -174,7 +178,7 @@ export class Driver {
}
const DetectorClass = module[config.className];
if (!DetectorClass) {
throw new Error(
throw ExecutionException.make(
`Detector class ${config.className} not found in module ${config.modulePath}`,
);
}
Expand All @@ -183,7 +187,9 @@ export class Driver {
// Attempt to find a built-in detector
const detector = await findBuiltInDetector(this.ctx, config.className);
if (!detector) {
throw new Error(`Built-in detector ${config.className} not found`);
throw ExecutionException.make(
`Built-in detector ${config.className} not found`,
);
}
return detector;
}
Expand Down Expand Up @@ -496,11 +502,11 @@ export class Runner {
* Returns the result of the execution.
* @throws If the runner hasn't been executed.
*/
public getResult(): MistiResult {
public getResult(): MistiResult | never {
if (this.result !== undefined) {
return this.result;
} else {
throw new Error("Runner hasn't been executed");
throw InternalException.make("Runner hasn't been executed");
}
}

Expand All @@ -517,10 +523,12 @@ export class Runner {
*/
private static checkCLIOptions(options: CLIOptions) {
if (options.verbose === true && options.quiet === true) {
throw new Error(`Please choose only one option: --verbose or --quiet`);
throw ExecutionException.make(
`Please choose only one option: --verbose or --quiet`,
);
}
if (options.allDetectors === true && options.detectors !== undefined) {
throw new Error(
throw ExecutionException.make(
`--detectors and --all-detectors cannot be used simultaneously`,
);
}
Expand Down Expand Up @@ -573,10 +581,12 @@ class SingleContractProjectManager {
return configPath;
}

private extractContractName(): string {
private extractContractName(): string | never {
const fileName = this.contractPath.split("/").pop();
if (!fileName) {
throw new Error(`Invalid contract path: ${this.contractPath}`);
throw ExecutionException.make(
`Invalid contract path: ${this.contractPath}`,
);
}
return fileName.slice(0, -5);
}
Expand Down
11 changes: 5 additions & 6 deletions src/internals/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ExecutionException } from "./exceptions";
import { getEnabledDetectors, getAllDetectors } from "../detectors/detector";
import { z } from "zod";
import * as fs from "fs";
import { getEnabledDetectors, getAllDetectors } from "../detectors/detector";

interface DetectorConfig {
modulePath?: string; // Used only for custom out-of-tree detectors
Expand Down Expand Up @@ -55,7 +56,7 @@ export class MistiConfig {
configData = JSON.parse(configFileContents);
} catch (err) {
if (err instanceof Error) {
throw new Error(
throw ExecutionException.make(
`Could not load or parse config file (${configPath}): ${err.message}`,
);
} else {
Expand Down Expand Up @@ -91,9 +92,7 @@ export class MistiConfig {
this.verbosity = parsedConfig.verbosity;
} catch (err) {
if (err instanceof z.ZodError) {
throw new Error(
`Failed to initialize Config with provided data: ${err.message}`,
);
throw ExecutionException.make(`Configuration error: ${err.message}`);
} else {
throw err;
}
Expand All @@ -112,7 +111,7 @@ export class MistiConfig {
} else {
const parts = detector.split(":");
if (parts.length !== 2) {
throw new Error(
throw ExecutionException.make(
`Cannot find built-in or custom detector: ${detector}`,
);
}
Expand Down
43 changes: 33 additions & 10 deletions src/internals/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export class TactException {
}
}

/**
* Internal error, typically caused by a bug in Misti.
*/
export class InternalException {
private constructor() {}
static make(
Expand All @@ -73,7 +76,7 @@ export class InternalException {
}> = {},
): Error {
const { loc = undefined, node = undefined, generateReport = true } = params;
const locStr = this.makeLocationString(loc);
const locStr = makeLocationString(loc);
const errorKind = `Internal Misti Error${locStr}:`;
const fullMsg = [
errorKind,
Expand All @@ -94,19 +97,39 @@ export class InternalException {
// Display short message to the user.
return new Error([shortMsg, reportText].join("\n"));
}
}

static makeLocationString(loc: SrcInfo | undefined): string {
return loc
? (() => {
const { lineNum, colNum } = loc.interval.getLineAndColumn();
return lineNum !== 0 && colNum !== 0
? ` at ${lineNum}:${colNum}`
: "";
})()
: "";
/**
* An error caused by incorrect actions of the user, such as wrong configuration,
* problems in the environment, wrong CLI options.
*/
export class ExecutionException {
private constructor() {}
static make(
msg: string,
{
loc = undefined,
}: Partial<{
loc: SrcInfo;
node: AstNode;
}> = {},
): Error {
const locStr = makeLocationString(loc);
const errorKind = `Execution Error${locStr}:`;
const shortMsg = [errorKind, msg].join("\n");
return new Error(shortMsg);
}
}

function makeLocationString(loc: SrcInfo | undefined): string {
return loc
? (() => {
const { lineNum, colNum } = loc.interval.getLineAndColumn();
return lineNum !== 0 && colNum !== 0 ? ` at ${lineNum}:${colNum}` : "";
})()
: "";
}

/**
* Returns backtrace of the JS script upon execution.
*/
Expand Down
12 changes: 9 additions & 3 deletions src/internals/ir/builders/tactIRBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
} from "..";
import { MistiContext } from "../../context";
import { formatPosition } from "../../tactASTUtil";
import { TactException, InternalException } from "../../exceptions";
import {
TactException,
InternalException,
ExecutionException,
} from "../../exceptions";
import {
Config as TactConfig,
ConfigProject,
Expand Down Expand Up @@ -818,12 +822,14 @@ class TactConfigManager {
private readTactConfig(): TactConfig {
const resolvedPath = path.resolve(this.tactConfigPath);
if (!fs.existsSync(resolvedPath)) {
throw new Error(`Unable to find config file at ${resolvedPath}`);
throw ExecutionException.make(
`Unable to find config file at ${resolvedPath}`,
);
}
try {
return parseConfig(fs.readFileSync(resolvedPath, "utf8"));
} catch (err) {
throw new Error(
throw ExecutionException.make(
`Unable to parse config file at ${resolvedPath}:\n${err}`,
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/internals/solver/souffle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MistiContext } from "../context";
import { CFG, BasicBlockIdx, CompilationUnit } from "../ir";
import { InternalException } from "../exceptions";
import { SolverResults } from "./results";
import { Solver } from "./solver";
import {
Expand Down Expand Up @@ -140,7 +141,7 @@ export class SouffleSolver<State> implements Solver<State> {
this.mapper.addConstraints(ctx);
const result = this.execute(ctx);
if (result.kind !== "structured") {
throw new Error(
throw InternalException.make(
`Error executing Soufflé:\n${result.kind === "error" ? result.stderr : "Cannot unmarshal raw output:\n" + result.results}`,
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/internals/tactASTUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export function foldExpressions<T>(
});
break;
default:
throw new Error("Unsupported statement");
throw InternalException.make("Unsupported statement");
}
return acc;
}
Expand Down

0 comments on commit c8eb532

Please sign in to comment.