[babel] Add panicOnBailout option
Adds an option to always throw errors regardless of severity (default, ie the status quo), or when the flag is disabled, only critical errors will be thrown. Any error that isn't considered a critical error (see `CompilerError.isCritical()`) since it might indicate that the compiler is buggy, while non-critical errors will result in that file being skipped for compilation, but otherwise continue compiling other files
Lauren Tan committed
Apr 20, 2023 at 16:21 UTC
c228d63f0375b0051b4ce18d2e748886ae292ba8
7 files changed
+72
-5
compiler/forget/src/Babel/BabelPlugin.ts
+21
-4
@@ -54,7 +54,6 @@ export default function ReactForgetBabelPlugin(
54
fn: BabelCore.NodePath<t.FunctionDeclaration>,
55
pass: BabelPluginPass
56
): void {
57
- hasForgetCompiledCode = true;
57
const compiled = compile(fn, pass.opts.environment);
58
59
if (pass.opts.gating != null) {
@@ -92,6 +91,8 @@ export default function ReactForgetBabelPlugin(
91
fn.replaceWith(compiled);
92
}
93
94
+ hasForgetCompiledCode = true;
95
+
96
// We are generating a new FunctionDeclaration node, so we must skip over it or this
97
// traversal will loop infinitely.
98
fn.skip();
@@ -171,7 +172,7 @@ export default function ReactForgetBabelPlugin(
172
new CompilerErrorDetail({
173
reason,
174
description: violation.value.trim(),
174
- severity: ErrorSeverity.InvalidInput,
175
+ severity: ErrorSeverity.UnsafeInput,
176
codeframe: null,
177
loc: violation.loc ?? null,
178
})
@@ -179,7 +180,11 @@ export default function ReactForgetBabelPlugin(
180
}
181
182
if (fileHasUseForgetDirective) {
182
- throw error;
183
+ if (options.panicOnBailout || error.isCritical()) {
184
+ throw error;
185
+ } else {
186
+ console.error(error);
187
+ }
188
}
189
190
return;
@@ -245,7 +250,19 @@ export default function ReactForgetBabelPlugin(
250
if (options.logger && err) {
251
options.logger.logEvent("err", err);
252
}
248
- throw err;
253
+ /** Always throw if the flag is enabled, otherwise we only throw if the error is critical
254
+ * (eg an invariant is broken, meaning the compiler may be buggy). See
255
+ * {@link CompilerError.isCritical} for mappings.
256
+ * */
257
+ if (
258
+ options.panicOnBailout ||
259
+ !(err instanceof CompilerError) ||
260
+ (err instanceof CompilerError && err.isCritical())
261
+ ) {
262
+ throw err;
263
+ } else {
264
+ console.error(err);
265
+ }
266
}
267
},
268
},
compiler/forget/src/Babel/PluginOptions.ts
+3
@@ -49,6 +49,8 @@ export type PluginOptions = {
49
* var Foo = isForgetEnabled_Pokes() ? Foo_forget : Foo_uncompiled;
50
*/
51
gating: GatingOptions | null;
52
+
53
+ panicOnBailout: boolean;
54
};
55
56
export type Logger = {
@@ -57,6 +59,7 @@ export type Logger = {
59
60
export const defaultOptions: PluginOptions = {
61
enableOnlyOnUseForgetDirective: false,
62
+ panicOnBailout: true,
63
environment: null,
64
logger: null,
65
gating: null,
compiler/forget/src/CompilerError.ts
+44
@@ -12,8 +12,24 @@ import { ExtractClassProperties } from "./Utils/types";
12
import { assertExhaustive } from "./Utils/utils";
13
14
export enum ErrorSeverity {
15
+ /**
16
+ * Unexpected syntax or input that may not be safe to compile.
17
+ */
18
InvalidInput = "InvalidInput",
19
+ /**
20
+ * User code contains unsafe React patterns that might not be safe to compile, but does not mean
21
+ * the compiler has bugs. This typically means we can skip over the affected files, but other
22
+ * files without these errors can still be compiled.
23
+ */
24
+ UnsafeInput = "UnsafeInput",
25
+ /**
26
+ * Unhandled syntax that we don't support yet.
27
+ */
28
Todo = "Todo",
29
+ /**
30
+ * An unexpected internal error in the compiler that indicates critical issues that can panic
31
+ * the compiler.
32
+ */
33
Invariant = "Invariant",
34
}
35
@@ -35,6 +51,8 @@ function mapSeverityToErrorCtor(severity: ErrorSeverity): CompilerErrorKind {
51
return TodoError;
52
case ErrorSeverity.Invariant:
53
return InvariantError;
54
+ case ErrorSeverity.UnsafeInput:
55
+ return UnsafeInputError;
56
default:
57
assertExhaustive(severity, `Unhandled severity level: ${severity}`);
58
}
@@ -57,6 +75,12 @@ class InvariantError extends Error {
75
this.name = `${ErrorSeverity.Invariant}Error`;
76
}
77
}
78
+class UnsafeInputError extends Error {
79
+ constructor(message: string) {
80
+ super(message);
81
+ this.name = `${ErrorSeverity.UnsafeInput}Error`;
82
+ }
83
+}
84
85
export function tryPrintCodeFrame(
86
options: CompilerErrorOptions
@@ -203,4 +227,24 @@ export class CompilerError extends Error {
227
hasErrors(): boolean {
228
return this.details.length > 0;
229
}
230
+
231
+ /**
232
+ * An error is critical if it means the compiler has entered into a broken state and cannot
233
+ * continue safely. Other expected errors such as Todos mean that we can skip over that component
234
+ * but otherwise continue compiling the rest of the app.
235
+ */
236
+ isCritical(): boolean {
237
+ return this.details.some((detail) => {
238
+ switch (detail.severity) {
239
+ case ErrorSeverity.Invariant:
240
+ return true;
241
+ case ErrorSeverity.InvalidInput:
242
+ case ErrorSeverity.Todo:
243
+ case ErrorSeverity.UnsafeInput:
244
+ return false;
245
+ default:
246
+ assertExhaustive(detail.severity, "Unhandled error severity");
247
+ }
248
+ });
249
+ }
250
}
compiler/forget/src/__tests__/compiler-test.ts
+1
@@ -58,6 +58,7 @@ describe("React Forget", () => {
58
},
59
logger: null,
60
gating: options.gating,
61
+ panicOnBailout: true,
62
}).code,
63
});
64
} catch (e) {
compiler/forget/src/__tests__/disableMemoizeJsxElements-test.ts
+1
@@ -54,6 +54,7 @@ describe("React Forget (Disable memoization of JSX elements)", () => {
54
},
55
logger: null,
56
gating: options.gating,
57
+ panicOnBailout: true,
58
}).code,
59
});
60
} catch (e) {
compiler/forget/src/__tests__/fixtures/compiler/error.sketchy-code-use-forget.expect.md
+1
-1
@@ -16,7 +16,7 @@ function lowercasecomponent() {
16
## Error
17
18
```
19
-[ReactForget] InvalidInput: Skipped compilation as it disables one or more React eslint rules. eslint-disable react-hooks/rules-of-hooks (1:1)
19
+[ReactForget] UnsafeInput: Skipped compilation as it disables one or more React eslint rules. eslint-disable react-hooks/rules-of-hooks (1:1)
20
```
21
22
\ No newline at end of file
compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts
+1
@@ -127,6 +127,7 @@ export default function generateTestsFromFixtures(
127
enableOnlyOnUseForgetDirective,
128
gating,
129
language: parseLanguage(input),
130
+ panicOnBailout: true,
131
});
132
} else {
133
receivedOutput = "<<input deleted>>";