@samitouri / QOS-React / commits / c228d63f03

[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>>";