Refactor CompilerError
This commit repurposes CompilerError to represent an aggregate of error details accumulated during HIR lowering. It also fixes the playground to correctly render errors again.
Lauren Tan committed
Jan 24, 2023 at 12:44 UTC
109d7ba9801af550b815b2dd46e6d7a09cb99e5e
7 files changed
+85
-51
compiler/forget/packages/playground/components/Editor/Output.tsx
+6
-2
@@ -146,8 +146,12 @@ function Output({ store }: Props) {
146
const compilerOutput = useMemo(() => compile(store.source), [store.source]);
147
148
if (typeof compilerOutput === "string") {
149
- if (compilerOutput === "") return <div></div>;
150
- return <div>error: ${compilerOutput}</div>;
149
+ if (compilerOutput === "") return <></>;
150
+ return (
151
+ <pre>
152
+ <code>${compilerOutput}</code>
153
+ </pre>
154
+ );
155
}
156
157
return (
compiler/forget/src/CompilerError.ts
+59
-33
@@ -1,5 +1,4 @@
1
import { Node, NodePath } from "@babel/core";
2
-import type { ExtractClassProperties } from "./Utils/types";
2
import { assertExhaustive } from "./Utils/utils";
3
4
export enum ErrorSeverity {
@@ -7,9 +6,24 @@ export enum ErrorSeverity {
6
Todo = "Todo",
7
}
8
10
-export type CompilerErrorOptions = ExtractClassProperties<CompilerError>;
9
+export type CompilerErrorOptions = {
10
+ reason: string;
11
+ severity: ErrorSeverity;
12
+ nodePath: AnyNodePath | null;
13
+};
14
type AnyNodePath = NodePath<Node | null | undefined>;
15
+type CompilerErrorKind = typeof InvalidInputError | typeof TodoError;
16
17
+function mapSeverityToErrorCtor(severity: ErrorSeverity): CompilerErrorKind {
18
+ switch (severity) {
19
+ case ErrorSeverity.InvalidInput:
20
+ return InvalidInputError;
21
+ case ErrorSeverity.Todo:
22
+ return TodoError;
23
+ default:
24
+ assertExhaustive(severity, `Unhandled severity level: ${severity}`);
25
+ }
26
+}
27
class InvalidInputError extends Error {
28
constructor(message: string) {
29
super(message);
@@ -23,30 +37,13 @@ class TodoError extends Error {
37
}
38
}
39
26
-function mapSeverityToErrorCtor(severity: ErrorSeverity) {
27
- switch (severity) {
28
- case ErrorSeverity.InvalidInput:
29
- return InvalidInputError;
30
- case ErrorSeverity.Todo:
31
- return TodoError;
32
- default:
33
- assertExhaustive(severity, `Unhandled severity level: ${severity}`);
34
- }
35
-}
36
-
37
-function printPathCodeFrame(
38
- reason: string,
39
- severity: ErrorSeverity,
40
- path: AnyNodePath
41
-) {
42
- return path
43
- .buildCodeFrameError(reason, mapSeverityToErrorCtor(severity))
44
- .toString();
45
-}
46
-
47
-export class CompilerError {
48
- severity: ErrorSeverity;
40
+/**
41
+ * Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
42
+ * aggregated into a single {@link CompilerError} later.
43
+ */
44
+export class CompilerErrorDetail {
45
reason: string;
46
+ severity: ErrorSeverity;
47
/**
48
* If a NodePath is provided, we will prefer Babel's built in codeframe error generation which
49
* will print error markers in the correct location.
@@ -54,20 +51,49 @@ export class CompilerError {
51
nodePath: AnyNodePath | null;
52
53
constructor(options: CompilerErrorOptions) {
57
- this.severity = options.severity;
54
this.reason = options.reason;
55
+ this.severity = options.severity;
56
this.nodePath = options.nodePath;
57
}
58
62
- toString(): string {
63
- const buffer = [];
64
- if (this.nodePath != null) {
59
+ get errorMessage(): string {
60
+ const buffer = [`${this.severity}: ${this.reason}`];
61
+ if (this.nodePath != null && this.nodePath.node?.loc != null) {
62
buffer.push(
66
- printPathCodeFrame(this.reason, this.severity, this.nodePath)
63
+ ` (${this.nodePath.node.loc.start.line}:${this.nodePath.node.loc.end.line})`
64
);
68
- } else {
69
- buffer.push(`${this.severity}: ${this.reason}`);
65
}
71
- return `[ReactForget] ${buffer.join("")}`;
66
+ return buffer.join("");
67
+ }
68
+
69
+ get codeFrame() {
70
+ if (this.nodePath == null) {
71
+ return this.errorMessage;
72
+ }
73
+ try {
74
+ return this.nodePath
75
+ .buildCodeFrameError(this.reason, mapSeverityToErrorCtor(this.severity))
76
+ .toString();
77
+ } catch {
78
+ return this.errorMessage;
79
+ }
80
+ }
81
+
82
+ toString(): string {
83
+ return `[ReactForget] ${this.errorMessage}`;
84
+ }
85
+}
86
+
87
+export class CompilerError extends Error {
88
+ details: CompilerErrorDetail[] = [];
89
+
90
+ constructor(details: CompilerErrorDetail[], ...args: any[]) {
91
+ super(...args);
92
+ this.details = details;
93
+ this.message = this.toString();
94
+ }
95
+
96
+ override toString() {
97
+ return this.details.map((detail) => detail.toString()).join("\n\n");
98
}
99
}
compiler/forget/src/CompilerPipeline.ts
+3
-4
@@ -6,8 +6,8 @@
6
*/
7
import { NodePath } from "@babel/traverse";
8
import * as t from "@babel/types";
9
+import { CompilerError } from "./CompilerError";
10
import {
10
- Environment,
11
HIRFunction,
12
lower,
13
mergeConsecutiveBlocks,
@@ -46,9 +46,8 @@ export type CompilerPipelineValue =
46
export function* run(
47
func: NodePath<t.FunctionDeclaration>
48
): Generator<CompilerPipelineValue, t.Function> {
49
- const lowering = lower(func).orElse((errors) => {
50
- const msg = errors.map((error) => error.toString()).join("\n\n");
51
- throw new Error(msg);
49
+ const lowering = lower(func).orElse((details) => {
50
+ throw new CompilerError(details);
51
});
52
53
const hir = lowering.unwrap();
compiler/forget/src/HIR/BuildHIR.ts
+2
-2
@@ -7,7 +7,7 @@
7
8
import { NodePath, Scope } from "@babel/traverse";
9
import * as t from "@babel/types";
10
-import { CompilerError, ErrorSeverity } from "../CompilerError";
10
+import { CompilerErrorDetail, ErrorSeverity } from "../CompilerError";
11
import { Err, Ok, Result } from "../lib/Result";
12
import { assertExhaustive } from "../Utils/utils";
13
import {
@@ -47,7 +47,7 @@ import HIRBuilder, { Environment } from "./HIRBuilder";
47
*/
48
export function lower(
49
func: NodePath<t.Function>
50
-): Result<HIRFunction, CompilerError[]> {
50
+): Result<HIRFunction, CompilerErrorDetail[]> {
51
const env = new Environment();
52
const builder = new HIRBuilder(env);
53
compiler/forget/src/HIR/HIRBuilder.ts
+6
-6
@@ -8,7 +8,7 @@
8
import { NodePath } from "@babel/traverse";
9
import * as t from "@babel/types";
10
import invariant from "invariant";
11
-import { CompilerError, CompilerErrorOptions } from "../CompilerError";
11
+import { CompilerErrorDetail, CompilerErrorOptions } from "../CompilerError";
12
import { logHIR } from "../Utils/logger";
13
import { assertExhaustive } from "../Utils/utils";
14
import { getOrAddGlobal } from "./Globals";
@@ -87,7 +87,7 @@ export default class HIRBuilder {
87
#bindings: Map<string, { node: t.Identifier; identifier: Identifier }> =
88
new Map();
89
#env: Environment;
90
- errors: CompilerError[] = [];
90
+ errors: CompilerErrorDetail[] = [];
91
92
get nextIdentifierId() {
93
return this.#env.nextIdentifierId;
@@ -415,10 +415,10 @@ export default class HIRBuilder {
415
invariant(false, "Expected a loop to be in scope");
416
}
417
418
- pushError(options: CompilerErrorOptions): CompilerError {
419
- const error = new CompilerError(options);
420
- this.errors.push(error);
421
- return error;
418
+ pushError(options: CompilerErrorOptions): CompilerErrorDetail {
419
+ const detail = new CompilerErrorDetail(options);
420
+ this.errors.push(detail);
421
+ return detail;
422
}
423
424
hasErrors(): boolean {
compiler/forget/src/__tests__/hir-test.ts
+8
-4
@@ -10,6 +10,7 @@
10
import { wasmFolder } from "@hpcc-js/wasm";
11
import path from "path";
12
import runReactForgetBabelPlugin from "../Babel/RunReactForgetBabelPlugin";
13
+import { CompilerError } from "../CompilerError";
14
import { toggleLogging } from "../Utils/logger";
15
import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
16
@@ -98,10 +99,13 @@ ${outputs.join("\n")}
99
});
100
101
function formatErrorOutput(error: Error): string {
101
- // Babel outputs absolute paths of the filename in the error mesage, which means fixtures will
102
- // contain paths that only pertain to your local machine. Strip it just here because that info
103
- // is still useful in real world usage of the Babel plugin.
104
- error.message = error.message.replace(/^\/.*?:\s/, "");
102
+ if (error instanceof CompilerError) {
103
+ error.message = error.details
104
+ .map((detail) => `[ReactForget] ${detail.codeFrame}`)
105
+ .join("\n\n");
106
+ } else {
107
+ error.message = error.message.replace(/^\/.*?:\s/, "");
108
+ }
109
return `
110
## Error
111
compiler/forget/src/index.ts
+1
@@ -5,6 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
export { default as BabelPlugin } from "./Babel/BabelPlugin";
8
+export { ErrorSeverity } from "./CompilerError";
9
export { compile, run } from "./CompilerPipeline";
10
export { printHIR } from "./HIR";
11
export { printReactiveFunction } from "./ReactiveScopes";