diff --git a/compiler/forget/packages/playground/components/Editor/Output.tsx b/compiler/forget/packages/playground/components/Editor/Output.tsx index 9ba6de1b82..314d9fe59e 100644 --- a/compiler/forget/packages/playground/components/Editor/Output.tsx +++ b/compiler/forget/packages/playground/components/Editor/Output.tsx @@ -146,8 +146,12 @@ function Output({ store }: Props) { const compilerOutput = useMemo(() => compile(store.source), [store.source]); if (typeof compilerOutput === "string") { - if (compilerOutput === "") return
; - return
error: ${compilerOutput}
; + if (compilerOutput === "") return <>; + return ( +
+        ${compilerOutput}
+      
+ ); } return ( diff --git a/compiler/forget/src/CompilerError.ts b/compiler/forget/src/CompilerError.ts index 3be1020c08..802c0d579e 100644 --- a/compiler/forget/src/CompilerError.ts +++ b/compiler/forget/src/CompilerError.ts @@ -1,5 +1,4 @@ import { Node, NodePath } from "@babel/core"; -import type { ExtractClassProperties } from "./Utils/types"; import { assertExhaustive } from "./Utils/utils"; export enum ErrorSeverity { @@ -7,9 +6,24 @@ export enum ErrorSeverity { Todo = "Todo", } -export type CompilerErrorOptions = ExtractClassProperties; +export type CompilerErrorOptions = { + reason: string; + severity: ErrorSeverity; + nodePath: AnyNodePath | null; +}; type AnyNodePath = NodePath; +type CompilerErrorKind = typeof InvalidInputError | typeof TodoError; +function mapSeverityToErrorCtor(severity: ErrorSeverity): CompilerErrorKind { + switch (severity) { + case ErrorSeverity.InvalidInput: + return InvalidInputError; + case ErrorSeverity.Todo: + return TodoError; + default: + assertExhaustive(severity, `Unhandled severity level: ${severity}`); + } +} class InvalidInputError extends Error { constructor(message: string) { super(message); @@ -23,30 +37,13 @@ class TodoError extends Error { } } -function mapSeverityToErrorCtor(severity: ErrorSeverity) { - switch (severity) { - case ErrorSeverity.InvalidInput: - return InvalidInputError; - case ErrorSeverity.Todo: - return TodoError; - default: - assertExhaustive(severity, `Unhandled severity level: ${severity}`); - } -} - -function printPathCodeFrame( - reason: string, - severity: ErrorSeverity, - path: AnyNodePath -) { - return path - .buildCodeFrameError(reason, mapSeverityToErrorCtor(severity)) - .toString(); -} - -export class CompilerError { - severity: ErrorSeverity; +/** + * Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then + * aggregated into a single {@link CompilerError} later. + */ +export class CompilerErrorDetail { reason: string; + severity: ErrorSeverity; /** * If a NodePath is provided, we will prefer Babel's built in codeframe error generation which * will print error markers in the correct location. @@ -54,20 +51,49 @@ export class CompilerError { nodePath: AnyNodePath | null; constructor(options: CompilerErrorOptions) { - this.severity = options.severity; this.reason = options.reason; + this.severity = options.severity; this.nodePath = options.nodePath; } - toString(): string { - const buffer = []; - if (this.nodePath != null) { + get errorMessage(): string { + const buffer = [`${this.severity}: ${this.reason}`]; + if (this.nodePath != null && this.nodePath.node?.loc != null) { buffer.push( - printPathCodeFrame(this.reason, this.severity, this.nodePath) + ` (${this.nodePath.node.loc.start.line}:${this.nodePath.node.loc.end.line})` ); - } else { - buffer.push(`${this.severity}: ${this.reason}`); } - return `[ReactForget] ${buffer.join("")}`; + return buffer.join(""); + } + + get codeFrame() { + if (this.nodePath == null) { + return this.errorMessage; + } + try { + return this.nodePath + .buildCodeFrameError(this.reason, mapSeverityToErrorCtor(this.severity)) + .toString(); + } catch { + return this.errorMessage; + } + } + + toString(): string { + return `[ReactForget] ${this.errorMessage}`; + } +} + +export class CompilerError extends Error { + details: CompilerErrorDetail[] = []; + + constructor(details: CompilerErrorDetail[], ...args: any[]) { + super(...args); + this.details = details; + this.message = this.toString(); + } + + override toString() { + return this.details.map((detail) => detail.toString()).join("\n\n"); } } diff --git a/compiler/forget/src/CompilerPipeline.ts b/compiler/forget/src/CompilerPipeline.ts index deaeef6d30..0f70eeaf21 100644 --- a/compiler/forget/src/CompilerPipeline.ts +++ b/compiler/forget/src/CompilerPipeline.ts @@ -6,8 +6,8 @@ */ import { NodePath } from "@babel/traverse"; import * as t from "@babel/types"; +import { CompilerError } from "./CompilerError"; import { - Environment, HIRFunction, lower, mergeConsecutiveBlocks, @@ -46,9 +46,8 @@ export type CompilerPipelineValue = export function* run( func: NodePath ): Generator { - const lowering = lower(func).orElse((errors) => { - const msg = errors.map((error) => error.toString()).join("\n\n"); - throw new Error(msg); + const lowering = lower(func).orElse((details) => { + throw new CompilerError(details); }); const hir = lowering.unwrap(); diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index 6c7dec45e8..9a69f99560 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -7,7 +7,7 @@ import { NodePath, Scope } from "@babel/traverse"; import * as t from "@babel/types"; -import { CompilerError, ErrorSeverity } from "../CompilerError"; +import { CompilerErrorDetail, ErrorSeverity } from "../CompilerError"; import { Err, Ok, Result } from "../lib/Result"; import { assertExhaustive } from "../Utils/utils"; import { @@ -47,7 +47,7 @@ import HIRBuilder, { Environment } from "./HIRBuilder"; */ export function lower( func: NodePath -): Result { +): Result { const env = new Environment(); const builder = new HIRBuilder(env); diff --git a/compiler/forget/src/HIR/HIRBuilder.ts b/compiler/forget/src/HIR/HIRBuilder.ts index 831a18ea74..04c321366a 100644 --- a/compiler/forget/src/HIR/HIRBuilder.ts +++ b/compiler/forget/src/HIR/HIRBuilder.ts @@ -8,7 +8,7 @@ import { NodePath } from "@babel/traverse"; import * as t from "@babel/types"; import invariant from "invariant"; -import { CompilerError, CompilerErrorOptions } from "../CompilerError"; +import { CompilerErrorDetail, CompilerErrorOptions } from "../CompilerError"; import { logHIR } from "../Utils/logger"; import { assertExhaustive } from "../Utils/utils"; import { getOrAddGlobal } from "./Globals"; @@ -87,7 +87,7 @@ export default class HIRBuilder { #bindings: Map = new Map(); #env: Environment; - errors: CompilerError[] = []; + errors: CompilerErrorDetail[] = []; get nextIdentifierId() { return this.#env.nextIdentifierId; @@ -415,10 +415,10 @@ export default class HIRBuilder { invariant(false, "Expected a loop to be in scope"); } - pushError(options: CompilerErrorOptions): CompilerError { - const error = new CompilerError(options); - this.errors.push(error); - return error; + pushError(options: CompilerErrorOptions): CompilerErrorDetail { + const detail = new CompilerErrorDetail(options); + this.errors.push(detail); + return detail; } hasErrors(): boolean { diff --git a/compiler/forget/src/__tests__/hir-test.ts b/compiler/forget/src/__tests__/hir-test.ts index 9880e80b44..6d6bb4fe7c 100644 --- a/compiler/forget/src/__tests__/hir-test.ts +++ b/compiler/forget/src/__tests__/hir-test.ts @@ -10,6 +10,7 @@ import { wasmFolder } from "@hpcc-js/wasm"; import path from "path"; import runReactForgetBabelPlugin from "../Babel/RunReactForgetBabelPlugin"; +import { CompilerError } from "../CompilerError"; import { toggleLogging } from "../Utils/logger"; import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures"; @@ -98,10 +99,13 @@ ${outputs.join("\n")} }); function formatErrorOutput(error: Error): string { - // Babel outputs absolute paths of the filename in the error mesage, which means fixtures will - // contain paths that only pertain to your local machine. Strip it just here because that info - // is still useful in real world usage of the Babel plugin. - error.message = error.message.replace(/^\/.*?:\s/, ""); + if (error instanceof CompilerError) { + error.message = error.details + .map((detail) => `[ReactForget] ${detail.codeFrame}`) + .join("\n\n"); + } else { + error.message = error.message.replace(/^\/.*?:\s/, ""); + } return ` ## Error diff --git a/compiler/forget/src/index.ts b/compiler/forget/src/index.ts index a3c6d8e2e3..a2ca50dc0c 100644 --- a/compiler/forget/src/index.ts +++ b/compiler/forget/src/index.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ export { default as BabelPlugin } from "./Babel/BabelPlugin"; +export { ErrorSeverity } from "./CompilerError"; export { compile, run } from "./CompilerPipeline"; export { printHIR } from "./HIR"; export { printReactiveFunction } from "./ReactiveScopes";