@samitouri / QOS-React-2 / commits / 00cb557b12

Add runtime validation for EnvironmentConfig

Sathya Gunasekaran committed Nov 7, 2023 at 11:04 UTC 00cb557b12ecc3991ed35570f69b3f0284a9a545
9 files changed +137 -151
compiler/apps/playground/components/Editor/EditorImpl.tsx
+13 -3
@@ -39,7 +39,7 @@ import {
39 } from "./Output";
40
41 function parseFunctions(
42 - source: string
42 + source: string,
43 ): Array<NodePath<t.FunctionDeclaration>> {
44 const items: Array<NodePath<t.FunctionDeclaration>> = [];
45 try {
@@ -77,6 +77,8 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
77 {
78 valueKind: ValueKind.Frozen,
79 effectKind: Effect.Freeze,
80 + noAlias: true,
81 + transitiveMixedData: true,
82 },
83 ],
84 [
@@ -84,6 +86,8 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
86 {
87 valueKind: ValueKind.Frozen,
88 effectKind: Effect.Freeze,
89 + noAlias: true,
90 + transitiveMixedData: true,
91 },
92 ],
93 [
@@ -91,6 +95,8 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
95 {
96 valueKind: ValueKind.Frozen,
97 effectKind: Effect.Freeze,
98 + noAlias: true,
99 + transitiveMixedData: true,
100 },
101 ],
102 [
@@ -98,6 +104,8 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
104 {
105 valueKind: ValueKind.Frozen,
106 effectKind: Effect.Freeze,
107 + noAlias: true,
108 + transitiveMixedData: true,
109 },
110 ],
111 [
@@ -105,6 +113,8 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
113 {
114 valueKind: ValueKind.Frozen,
115 effectKind: Effect.Freeze,
116 + noAlias: true,
117 + transitiveMixedData: true,
118 },
119 ],
120 ];
@@ -199,7 +209,7 @@ export default function Editor() {
209 const { enqueueSnackbar } = useSnackbar();
210 const compilerOutput = useMemo(
211 () => compile(deferredStore.source),
202 - [deferredStore.source]
212 + [deferredStore.source],
213 );
214
215 useMountEffect(() => {
@@ -213,7 +223,7 @@ export default function Editor() {
223 ...createMessage(
224 "Bad URL - fell back to the default Playground.",
225 MessageLevel.Info,
216 - MessageSource.Playground
226 + MessageSource.Playground,
227 ),
228 });
229 mountStore = defaultStore;
compiler/packages/babel-plugin-react-forget/scripts/jest/makeTransform.ts
+7 -5
@@ -7,14 +7,17 @@
7
8 import { jsx } from "@babel/plugin-syntax-jsx";
9 import babelJest from "babel-jest";
10 -import { DEFAULT_ENVIRONMENT_CONFIG, compile } from "babel-plugin-react-forget";
10 +import { compile } from "babel-plugin-react-forget";
11 import { execSync } from "child_process";
12
13 import type { NodePath, Visitor } from "@babel/traverse";
14 import type { CallExpression, FunctionDeclaration } from "@babel/types";
15 import * as t from "@babel/types";
16 +import {
17 + EnvironmentConfig,
18 + validateEnvironmentConfig,
19 +} from "babel-plugin-react-forget";
20 import { basename } from "path";
17 -import { EnvironmentConfig } from "../../src/HIR/Environment";
21
22 /**
23 * -- IMPORTANT --
@@ -23,10 +26,9 @@ import { EnvironmentConfig } from "../../src/HIR/Environment";
26 * as our script files are currently not used for babel cache breaking!!
27 */
28 const e2eTransformerCacheKey = 1;
26 -const forgetOptions: EnvironmentConfig = {
27 - ...DEFAULT_ENVIRONMENT_CONFIG,
29 +const forgetOptions: EnvironmentConfig = validateEnvironmentConfig({
30 enableAssumeHooksFollowRulesOfReact: true,
29 -};
31 +});
32 const debugMode = process.env["DEBUG_FORGET_COMPILER"] != null;
33
34 module.exports = (useForget: boolean) => {
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Imports.ts
+2 -2
@@ -8,9 +8,9 @@
8 import { NodePath } from "@babel/core";
9 import * as t from "@babel/types";
10 import { CompilerError } from "../CompilerError";
11 -import { GeneratedSource } from "../HIR";
11 +import { ExternalFunction, GeneratedSource } from "../HIR";
12 import { getOrInsertDefault } from "../Utils/utils";
13 -import { ExternalFunction, PluginOptions } from "./Options";
13 +import { PluginOptions } from "./Options";
14
15 export function addImportsToProgram(
16 path: NodePath<t.Program>,
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Options.ts
+2 -33
@@ -6,39 +6,8 @@
6 */
7
8 import * as t from "@babel/types";
9 -import { z } from "zod";
10 -import { CompilerError, CompilerErrorDetailOptions } from "../CompilerError";
11 -import { PartialEnvironmentConfig } from "../HIR/Environment";
12 -
13 -import { fromZodError } from "zod-validation-error";
14 -
15 -export const ExternalFunctionSchema = z.object({
16 - // Source for the imported module that exports the `importSpecifierName` functions
17 - source: z.string(),
18 -
19 - // Unique name for the feature flag test condition, eg `isForgetEnabled_ProjectName`
20 - importSpecifierName: z.string(),
21 -});
22 -
23 -export function tryParseExternalFunction(
24 - maybeExternalFunction: any
25 -): ExternalFunction {
26 - const externalFunction = ExternalFunctionSchema.safeParse(
27 - maybeExternalFunction
28 - );
29 - if (externalFunction.success) {
30 - return externalFunction.data;
31 - }
32 -
33 - CompilerError.invalidConfig({
34 - reason: `${fromZodError(externalFunction.error)}`,
35 - description: "Update Forget config to fix the error",
36 - loc: null,
37 - suggestions: null,
38 - });
39 -}
40 -
41 -export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
9 +import { CompilerErrorDetailOptions } from "../CompilerError";
10 +import { ExternalFunction, PartialEnvironmentConfig } from "../HIR/Environment";
11
12 export type PanicThresholdOptions =
13 // Any errors will panic the compiler by throwing an exception, which will
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts
+6 -7
@@ -13,19 +13,18 @@ import {
13 CompilerSuggestionOperation,
14 ErrorSeverity,
15 } from "../CompilerError";
16 -import { validateEnvironmentConfig } from "../HIR/Environment";
16 +import {
17 + ExternalFunction,
18 + tryParseExternalFunction,
19 + validateEnvironmentConfig,
20 +} from "../HIR/Environment";
21 import { CodegenFunction } from "../ReactiveScopes";
22 import { isComponentDeclaration } from "../Utils/ComponentDeclaration";
23 import { assertExhaustive } from "../Utils/utils";
24 import { insertGatedFunctionDeclaration } from "./Gating";
25 import { addImportsToProgram, updateUseMemoCacheImport } from "./Imports";
26 import { addInstrumentForget } from "./Instrumentation";
23 -import {
24 - ExternalFunction,
25 - PluginOptions,
26 - parsePluginOptions,
27 - tryParseExternalFunction,
28 -} from "./Options";
27 +import { PluginOptions, parsePluginOptions } from "./Options";
28 import { compileFn } from "./Pipeline";
29
30 export type CompilerPass = {
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+91 -91
@@ -6,8 +6,9 @@
6 */
7
8 import * as t from "@babel/types";
9 +import { z } from "zod";
10 +import { fromZodError } from "zod-validation-error";
11 import { CompilerError } from "../CompilerError";
10 -import { ExternalFunction } from "../Entrypoint/Options";
12 import { log } from "../Utils/logger";
13 import {
14 DEFAULT_GLOBALS,
@@ -36,26 +37,35 @@ import {
37 addHook,
38 } from "./ObjectShape";
39
39 -export type Hook = {
40 +export const ExternalFunctionSchema = z.object({
41 + // Source for the imported module that exports the `importSpecifierName` functions
42 + source: z.string(),
43 +
44 + // Unique name for the feature flag test condition, eg `isForgetEnabled_ProjectName`
45 + importSpecifierName: z.string(),
46 +});
47 +export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
48 +
49 +const HookSchema = z.object({
50 /**
51 * The effect of arguments to this hook. Describes whether the hook may or may
52 * not mutate arguments, etc.
53 */
44 - effectKind: Effect;
54 + effectKind: z.nativeEnum(Effect),
55
56 /**
57 * The kind of value returned by the hook. Allows indicating that a hook returns
58 * a primitive or already-frozen value, which can allow more precise memoization
59 * of callers.
60 */
51 - valueKind: ValueKind;
61 + valueKind: z.nativeEnum(ValueKind),
62
63 /**
64 * Specifies whether hook arguments may be aliased by other arguments or by the
65 * return value of the function. Defaults to false. When enabled, this allows the
66 * compiler to avoid memoizing arguments.
67 */
58 - noAlias?: boolean;
68 + noAlias: z.boolean().default(false),
69
70 /**
71 * Specifies whether the hook returns data that is composed of:
@@ -74,8 +84,10 @@ export type Hook = {
84 * like `data.items.map(...)` since these builtin types have few built-in
85 * methods.
86 */
77 - transitiveMixedData?: boolean;
78 -};
87 + transitiveMixedData: z.boolean().default(false),
88 +});
89 +
90 +export type Hook = z.infer<typeof HookSchema>;
91
92 // TODO(mofeiZ): User defined global types (with corresponding shapes).
93 // User defined global types should have inline ObjectShapes instead of directly
@@ -85,52 +97,42 @@ export type Hook = {
97 // missing required shapes (BuiltInArray for [] and BuiltInObject for {})
98 // missing some recursive Object / Function shapeIds
99
88 -export type EnvironmentConfig = {
89 - customHooks: Map<string, Hook> | null;
100 +const EnvironmentConfigSchema = z.object({
101 + customHooks: z.map(z.string(), HookSchema).nullish(),
102
103 // 🌲
92 - enableForest: boolean;
104 + enableForest: z.boolean().default(false),
105
106 /**
107 * Enable memoization of JSX elements in addition to other types of values. When disabled,
108 * other types (objects, arrays, call expressions, etc) are memoized, but not known JSX
109 * values.
98 - *
99 - * Defaults to true
110 */
101 - memoizeJsxElements: boolean;
111 + memoizeJsxElements: z.boolean().default(true),
112
113 /**
114 * Enable validation of hooks to partially check that the component honors the rules of hooks.
115 * When disabled, the component is assumed to follow the rules (though the Babel plugin looks
116 * for suppressions of the lint rule).
107 - *
108 - * Defaults to false
117 */
110 - validateHooksUsage: boolean;
118 + validateHooksUsage: z.boolean().default(true),
119
120 /**
121 * Validate that ref values (`ref.current`) are not accessed during render.
114 - *
115 - * Defaults to false
122 */
117 - validateRefAccessDuringRender: boolean;
123 + validateRefAccessDuringRender: z.boolean().default(false),
124
125 /**
126 * Validate that mutable lambdas are not passed where a frozen value is expected, since mutable
127 * lambdas cannot be frozen. The only mutation allowed inside a frozen lambda is of ref values.
122 - *
123 - * Defaults to false
128 */
125 - validateFrozenLambdas: boolean;
129 + validateFrozenLambdas: z.boolean().default(false),
130
131 /**
132 * Validates that setState is not unconditionally called during render, as it can lead to
133 * infinite loops.
130 - *
131 - * Defaults to false
134 */
133 - validateNoSetStateInRender: boolean;
135 + validateNoSetStateInRender: z.boolean().default(false),
136
137 /**
138 * When enabled, the compiler assumes that hooks follow the Rules of React:
@@ -138,19 +140,15 @@ export type EnvironmentConfig = {
140 * any arguments to a hook are assumed frozen after calling the hook.
141 * - Hooks may memoize the result they return, thus the return value is
142 * assumed frozen.
141 -
142 - * Defaults to false
143 */
144 - enableAssumeHooksFollowRulesOfReact: boolean;
144 + enableAssumeHooksFollowRulesOfReact: z.boolean().default(false),
145
146 /**
147 * When enabled, removes *all* memoization from the function: this includes
148 * removing manually added useMemo/useCallback as well as not adding Forget's
149 * usual useMemoCache-based memoization.
150 - *
151 - * Defaults to false (ie, by default memoization is enabled)
150 */
153 - disableAllMemoization: boolean;
151 + disableAllMemoization: z.boolean().default(false),
152
153 /**
154 * Enables codegen mutability debugging. This emits a dev-mode only to log mutations
@@ -173,7 +171,7 @@ export type EnvironmentConfig = {
171 * }
172 * }
173 */
176 - enableEmitFreeze: ExternalFunction | null;
174 + enableEmitFreeze: ExternalFunctionSchema.nullish(),
175
176 /**
177 * Forget infers certain operations as "freezing" a value, such that those
@@ -208,21 +206,17 @@ export type EnvironmentConfig = {
206 * are transitively frozen when the function itself is frozen. So in this case,
207 * `y` and `z` would be frozen when `x` is frozen.
208 */
211 - enableTransitivelyFreezeFunctionExpressions: boolean;
209 + enableTransitivelyFreezeFunctionExpressions: z.boolean().default(false),
210
211 /**
212 * Enable merging consecutive scopes that invalidate together.
215 - *
216 - * Defaults to false.
213 */
218 - enableMergeConsecutiveScopes: boolean;
214 + enableMergeConsecutiveScopes: z.boolean().default(true),
215
216 /**
217 * Enable validation of mutable ranges
222 - *
223 - * Defaults to false
218 */
225 - assertValidMutableRanges: boolean;
219 + assertValidMutableRanges: z.boolean().default(false),
220
221 /**
222 * Instead of handling holey arrays, bail out with a TODO error.
@@ -244,7 +238,7 @@ export type EnvironmentConfig = {
238 * PR that changed the AST definition
239 * https://github.com/babel/babel/pull/10917/files#diff-19b555d2f3904c206af406540d9df200b1e16befedb83ff39ebfcbd876f7fa8aL52-R56
240 */
247 - bailoutOnHoleyArrays: boolean;
241 + bailoutOnHoleyArrays: z.boolean().default(false),
242
243 /**
244 * Enable emitting "change variables" which store the result of whether a particular
@@ -264,55 +258,44 @@ export type EnvironmentConfig = {
258 * if ($[0] !== input) ...
259 * ```
260 */
267 - enableChangeVariableCodegen: boolean;
268 -};
269 -
270 -export const DEFAULT_ENVIRONMENT_CONFIG: Readonly<EnvironmentConfig> = {
271 - customHooks: null,
272 -
273 - memoizeJsxElements: true,
274 - validateHooksUsage: true,
275 - enableMergeConsecutiveScopes: true,
276 -
277 - assertValidMutableRanges: false,
278 - bailoutOnHoleyArrays: false,
279 - disableAllMemoization: false,
280 - enableAssumeHooksFollowRulesOfReact: false,
281 - enableEmitFreeze: null,
282 - enableForest: false,
283 - enableChangeVariableCodegen: false,
284 - enableTransitivelyFreezeFunctionExpressions: false,
285 -
286 - validateFrozenLambdas: false,
287 - validateNoSetStateInRender: false,
288 - validateRefAccessDuringRender: false,
289 -};
261 + enableChangeVariableCodegen: z.boolean().default(false),
262 +});
263 +
264 +export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
265
266 export function parseConfigPragma(pragma: string): EnvironmentConfig {
292 - const config = { ...DEFAULT_ENVIRONMENT_CONFIG };
293 - for (const key of Object.keys(DEFAULT_ENVIRONMENT_CONFIG)) {
294 - if (!isEnvironmentConfigKey(key)) {
267 + const maybeConfig: any = {};
268 + // Get the defaults to programmatically check for boolean properties
269 + const defaultConfig = EnvironmentConfigSchema.parse({});
270 +
271 + for (const token of pragma.split(" ")) {
272 + if (!token.startsWith("@")) {
273 continue;
274 }
297 - const value = config[key];
298 - if (typeof value !== "boolean") {
299 - // We only support setting boolean flags via pragma strings
275 + const keyVal = token.slice(1);
276 + let [key, val]: any = keyVal.split(":");
277 + if (typeof defaultConfig[key as keyof EnvironmentConfig] !== "boolean") {
278 + // skip parsing non-boolean properties
279 continue;
280 }
302 - if (pragma.includes(`@${key}:true`)) {
303 - config[key] = true as any;
304 - } else if (pragma.includes(`@${key}:false`)) {
305 - config[key] = false as any;
306 - } else if (pragma.includes(`@${key}`)) {
307 - config[key] = true as any;
281 + if (val === undefined || val === "true") {
282 + val = true;
283 + } else {
284 + val = false;
285 }
286 + maybeConfig[key] = val;
287 }
288
311 - return config;
312 -}
313 -
314 -function isEnvironmentConfigKey(key: string): key is keyof EnvironmentConfig {
315 - return Object.prototype.hasOwnProperty.call(DEFAULT_ENVIRONMENT_CONFIG, key);
289 + const config = EnvironmentConfigSchema.safeParse(maybeConfig);
290 + if (config.success) {
291 + return config.data;
292 + }
293 + CompilerError.invalidConfig({
294 + reason: `${fromZodError(config.error)}`,
295 + description: "Update Forget config to fix the error",
296 + loc: null,
297 + suggestions: null,
298 + });
299 }
300
301 export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
@@ -466,16 +449,33 @@ function isHookName(name: string): boolean {
449 export function validateEnvironmentConfig(
450 partialConfig: PartialEnvironmentConfig | null
451 ): EnvironmentConfig {
469 - const config: EnvironmentConfig = { ...DEFAULT_ENVIRONMENT_CONFIG };
470 - if (partialConfig != null) {
471 - for (const key of Object.keys(DEFAULT_ENVIRONMENT_CONFIG)) {
472 - if (!isEnvironmentConfigKey(key)) {
473 - continue;
474 - }
475 - if (Object.prototype.hasOwnProperty.call(partialConfig, key)) {
476 - config[key] = partialConfig[key] as any; // we know the key is present from hasOwnProperty
477 - }
478 - }
452 + const config = EnvironmentConfigSchema.safeParse(partialConfig);
453 + if (config.success) {
454 + return config.data;
455 + }
456 +
457 + CompilerError.invalidConfig({
458 + reason: `${fromZodError(config.error)}`,
459 + description: "Update Forget config to fix the error",
460 + loc: null,
461 + suggestions: null,
462 + });
463 +}
464 +
465 +export function tryParseExternalFunction(
466 + maybeExternalFunction: any
467 +): ExternalFunction {
468 + const externalFunction = ExternalFunctionSchema.safeParse(
469 + maybeExternalFunction
470 + );
471 + if (externalFunction.success) {
472 + return externalFunction.data;
473 }
480 - return config;
474 +
475 + CompilerError.invalidConfig({
476 + reason: `${fromZodError(externalFunction.error)}`,
477 + description: "Update Forget config to fix the error",
478 + loc: null,
479 + suggestions: null,
480 + });
481 }
compiler/packages/babel-plugin-react-forget/src/HIR/index.ts
+3 -1
@@ -11,10 +11,12 @@ export { assertValidMutableRanges } from "./AssertValidMutableRanges";
11 export { lower } from "./BuildHIR";
12 export { computeDominatorTree, computePostDominatorTree } from "./Dominator";
13 export {
14 - DEFAULT_ENVIRONMENT_CONFIG,
14 Environment,
15 + EnvironmentConfig,
16 + ExternalFunction,
17 Hook,
18 parseConfigPragma,
19 + validateEnvironmentConfig,
20 } from "./Environment";
21 export * from "./HIR";
22 export {
compiler/packages/babel-plugin-react-forget/src/__tests__/parseConfigPragma-test.ts
+10 -8
@@ -5,21 +5,23 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import { DEFAULT_ENVIRONMENT_CONFIG, parseConfigPragma } from "..";
8 +import { parseConfigPragma, validateEnvironmentConfig } from "..";
9
10 describe("parseConfigPragma()", () => {
11 it("parses flags in various forms", () => {
12 - const config = parseConfigPragma(
13 - "@enableForest @validateFrozenLambdas:true @memoizeJsxElements:false"
14 - );
12 + const defaultConfig = validateEnvironmentConfig({});
13 +
14 // Validate defaults first to make sure that the parser is getting the value from the pragma,
15 // and not just missing it and getting the default value
17 - expect(DEFAULT_ENVIRONMENT_CONFIG.enableForest).toBe(false);
18 - expect(DEFAULT_ENVIRONMENT_CONFIG.validateFrozenLambdas).toBe(false);
19 - expect(DEFAULT_ENVIRONMENT_CONFIG.memoizeJsxElements).toBe(true);
16 + expect(defaultConfig.enableForest).toBe(false);
17 + expect(defaultConfig.validateFrozenLambdas).toBe(false);
18 + expect(defaultConfig.memoizeJsxElements).toBe(true);
19
20 + const config = parseConfigPragma(
21 + "@enableForest @validateFrozenLambdas:true @memoizeJsxElements:false"
22 + );
23 expect(config).toEqual({
22 - ...DEFAULT_ENVIRONMENT_CONFIG,
24 + ...defaultConfig,
25 enableForest: true,
26 validateFrozenLambdas: true,
27 memoizeJsxElements: false,
compiler/packages/babel-plugin-react-forget/src/index.ts
+3 -1
@@ -20,13 +20,15 @@ export {
20 run,
21 } from "./Entrypoint";
22 export {
23 - DEFAULT_ENVIRONMENT_CONFIG,
23 Effect,
24 + EnvironmentConfig,
25 + ExternalFunction,
26 Hook,
27 SourceLocation,
28 ValueKind,
29 parseConfigPragma,
30 printHIR,
31 + validateEnvironmentConfig,
32 } from "./HIR";
33 export { printReactiveFunction } from "./ReactiveScopes";
34