@samitouri / QOS-React-1 / commits / 792fa065ca

[compiler][ez] Clean up pragma parsing for tests + playground (#31347)

Move environment config parsing for `inlineJsxTransform`, `lowerContextAccess`, and some dev-only options out of snap (test fixture). These should now be available for playground via `@inlineJsxTransform` and `lowerContextAccess`. Other small change: Changed zod fields from `nullish()` -> `nullable().default(null)`. [`nullish`](https://zod.dev/?id=nullish) fields accept `null | undefined` and default to `undefined`. We don't distinguish between null and undefined for any of these options, so let's only accept null + default to null. This also makes EnvironmentConfig in the playground more accurate. Previously, some fields just didn't show up as `prettyFormat({field: undefined})` does not print `field`.

mofeiZ committed Nov 5, 2024 at 18:19 UTC 792fa065ca7a46ce4a583e8f6f35eec8bd813d43
15 files changed +101 -114
compiler/apps/playground/components/Editor/EditorImpl.tsx
+2 -2
@@ -14,7 +14,7 @@ import {
14 CompilerErrorDetail,
15 Effect,
16 ErrorSeverity,
17 - parseConfigPragma,
17 + parseConfigPragmaForTests,
18 ValueKind,
19 runPlayground,
20 type Hook,
@@ -208,7 +208,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
208 try {
209 // Extract the first line to quickly check for custom test directives
210 const pragma = source.substring(0, source.indexOf('\n'));
211 - const config = parseConfigPragma(pragma);
211 + const config = parseConfigPragmaForTests(pragma);
212
213 for (const fn of parseFunctions(source, language)) {
214 const id = withIdentifier(getFunctionIdentifier(fn));
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+64 -31
@@ -69,8 +69,8 @@ export const ExternalFunctionSchema = z.object({
69 export const InstrumentationSchema = z
70 .object({
71 fn: ExternalFunctionSchema,
72 - gating: ExternalFunctionSchema.nullish(),
73 - globalGating: z.string().nullish(),
72 + gating: ExternalFunctionSchema.nullable(),
73 + globalGating: z.string().nullable(),
74 })
75 .refine(
76 opts => opts.gating != null || opts.globalGating != null,
@@ -147,7 +147,7 @@ export type Hook = z.infer<typeof HookSchema>;
147 */
148
149 const EnvironmentConfigSchema = z.object({
150 - customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
150 + customHooks: z.map(z.string(), HookSchema).default(new Map()),
151
152 /**
153 * A function that, given the name of a module, can optionally return a description
@@ -248,7 +248,7 @@ const EnvironmentConfigSchema = z.object({
248 *
249 * The symbol configuration is set for backwards compatability with pre-React 19 transforms
250 */
251 - inlineJsxTransform: ReactElementSymbolSchema.nullish(),
251 + inlineJsxTransform: ReactElementSymbolSchema.nullable().default(null),
252
253 /*
254 * Enable validation of hooks to partially check that the component honors the rules of hooks.
@@ -339,9 +339,9 @@ const EnvironmentConfigSchema = z.object({
339 * }
340 * }
341 */
342 - enableEmitFreeze: ExternalFunctionSchema.nullish(),
342 + enableEmitFreeze: ExternalFunctionSchema.nullable().default(null),
343
344 - enableEmitHookGuards: ExternalFunctionSchema.nullish(),
344 + enableEmitHookGuards: ExternalFunctionSchema.nullable().default(null),
345
346 /**
347 * Enable instruction reordering. See InstructionReordering.ts for the details
@@ -425,7 +425,7 @@ const EnvironmentConfigSchema = z.object({
425 * }
426 *
427 */
428 - enableEmitInstrumentForget: InstrumentationSchema.nullish(),
428 + enableEmitInstrumentForget: InstrumentationSchema.nullable().default(null),
429
430 // Enable validation of mutable ranges
431 assertValidMutableRanges: z.boolean().default(false),
@@ -464,8 +464,6 @@ const EnvironmentConfigSchema = z.object({
464 */
465 throwUnknownException__testonly: z.boolean().default(false),
466
467 - enableSharedRuntime__testonly: z.boolean().default(false),
468 -
467 /**
468 * Enables deps of a function epxression to be treated as conditional. This
469 * makes sure we don't load a dep when it's a property (to check if it has
@@ -503,7 +501,8 @@ const EnvironmentConfigSchema = z.object({
501 * computed one. This detects cases where rules of react violations may cause the
502 * compiled code to behave differently than the original.
503 */
506 - enableChangeDetectionForDebugging: ExternalFunctionSchema.nullish(),
504 + enableChangeDetectionForDebugging:
505 + ExternalFunctionSchema.nullable().default(null),
506
507 /**
508 * The react native re-animated library uses custom Babel transforms that
@@ -543,7 +542,7 @@ const EnvironmentConfigSchema = z.object({
542 *
543 * Here the variables `ref` and `myRef` will be typed as Refs.
544 */
546 - enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false),
545 + enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
546
547 /*
548 * If specified a value, the compiler lowers any calls to `useContext` to use
@@ -565,12 +564,57 @@ const EnvironmentConfigSchema = z.object({
564 * const {foo, bar} = useCompiledContext(MyContext, (c) => [c.foo, c.bar]);
565 * ```
566 */
568 - lowerContextAccess: ExternalFunctionSchema.nullish(),
567 + lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
568 });
569
570 export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
571
573 -export function parseConfigPragma(pragma: string): EnvironmentConfig {
572 +/**
573 + * For test fixtures and playground only.
574 + *
575 + * Pragmas are straightforward to parse for boolean options (`:true` and
576 + * `:false`). These are 'enabled' config values for non-boolean configs (i.e.
577 + * what is used when parsing `:true`).
578 + */
579 +const testComplexConfigDefaults: PartialEnvironmentConfig = {
580 + validateNoCapitalizedCalls: [],
581 + enableChangeDetectionForDebugging: {
582 + source: 'react-compiler-runtime',
583 + importSpecifierName: '$structuralCheck',
584 + },
585 + enableEmitFreeze: {
586 + source: 'react-compiler-runtime',
587 + importSpecifierName: 'makeReadOnly',
588 + },
589 + enableEmitInstrumentForget: {
590 + fn: {
591 + source: 'react-compiler-runtime',
592 + importSpecifierName: 'useRenderCounter',
593 + },
594 + gating: {
595 + source: 'react-compiler-runtime',
596 + importSpecifierName: 'shouldInstrument',
597 + },
598 + globalGating: '__DEV__',
599 + },
600 + enableEmitHookGuards: {
601 + source: 'react-compiler-runtime',
602 + importSpecifierName: '$dispatcherGuard',
603 + },
604 + inlineJsxTransform: {
605 + elementSymbol: 'react.transitional.element',
606 + globalDevVar: 'DEV',
607 + },
608 + lowerContextAccess: {
609 + source: 'react-compiler-runtime',
610 + importSpecifierName: 'useContext_withSelector',
611 + },
612 +};
613 +
614 +/**
615 + * For snap test fixtures and playground only.
616 + */
617 +export function parseConfigPragmaForTests(pragma: string): EnvironmentConfig {
618 const maybeConfig: any = {};
619 // Get the defaults to programmatically check for boolean properties
620 const defaultConfig = EnvironmentConfigSchema.parse({});
@@ -580,21 +624,12 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
624 continue;
625 }
626 const keyVal = token.slice(1);
583 - let [key, val]: any = keyVal.split(':');
584 -
585 - if (key === 'validateNoCapitalizedCalls') {
586 - maybeConfig[key] = [];
587 - continue;
588 - }
627 + let [key, val = undefined] = keyVal.split(':');
628 + const isSet = val === undefined || val === 'true';
629
590 - if (
591 - key === 'enableChangeDetectionForDebugging' &&
592 - (val === undefined || val === 'true')
593 - ) {
594 - maybeConfig[key] = {
595 - source: 'react-compiler-runtime',
596 - importSpecifierName: '$structuralCheck',
597 - };
630 + if (isSet && key in testComplexConfigDefaults) {
631 + maybeConfig[key] =
632 + testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
633 continue;
634 }
635
@@ -609,7 +644,6 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
644 props.push({type: 'name', name: elt});
645 }
646 }
612 - console.log([valSplit[0], props.map(x => x.name ?? '*').join('.')]);
647 maybeConfig[key] = [[valSplit[0], props]];
648 }
649 continue;
@@ -620,11 +654,10 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
654 continue;
655 }
656 if (val === undefined || val === 'true') {
623 - val = true;
657 + maybeConfig[key] = true;
658 } else {
625 - val = false;
659 + maybeConfig[key] = false;
660 }
627 - maybeConfig[key] = val;
661 }
662
663 const config = EnvironmentConfigSchema.safeParse(maybeConfig);
compiler/packages/babel-plugin-react-compiler/src/HIR/index.ts
+1 -1
@@ -17,7 +17,7 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR';
17 export {computeDominatorTree, computePostDominatorTree} from './Dominator';
18 export {
19 Environment,
20 - parseConfigPragma,
20 + parseConfigPragmaForTests,
21 validateEnvironmentConfig,
22 type EnvironmentConfig,
23 type ExternalFunction,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-emit-imports-same-source.expect.md
+2 -2
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @enableEmitFreeze @instrumentForget
5 +// @enableEmitFreeze @enableEmitInstrumentForget
6
7 function useFoo(props) {
8 return foo(props.x);
@@ -18,7 +18,7 @@ import {
18 shouldInstrument,
19 makeReadOnly,
20 } from "react-compiler-runtime";
21 -import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @instrumentForget
21 +import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @enableEmitInstrumentForget
22
23 function useFoo(props) {
24 if (__DEV__ && shouldInstrument)
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-emit-imports-same-source.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @enableEmitFreeze @instrumentForget
1 +// @enableEmitFreeze @enableEmitInstrumentForget
2
3 function useFoo(props) {
4 return foo(props.x);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-gating-test.expect.md
+2 -2
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @instrumentForget @compilationMode(annotation) @gating
5 +// @enableEmitInstrumentForget @compilationMode(annotation) @gating
6
7 function Bar(props) {
8 'use forget';
@@ -25,7 +25,7 @@ function Foo(props) {
25 ```javascript
26 import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
27 import { useRenderCounter, shouldInstrument } from "react-compiler-runtime";
28 -import { c as _c } from "react/compiler-runtime"; // @instrumentForget @compilationMode(annotation) @gating
28 +import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation) @gating
29 const Bar = isForgetEnabled_Fixtures()
30 ? function Bar(props) {
31 "use forget";
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-gating-test.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @instrumentForget @compilationMode(annotation) @gating
1 +// @enableEmitInstrumentForget @compilationMode(annotation) @gating
2
3 function Bar(props) {
4 'use forget';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-test.expect.md
+2 -2
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @instrumentForget @compilationMode(annotation)
5 +// @enableEmitInstrumentForget @compilationMode(annotation)
6
7 function Bar(props) {
8 'use forget';
@@ -24,7 +24,7 @@ function Foo(props) {
24
25 ```javascript
26 import { useRenderCounter, shouldInstrument } from "react-compiler-runtime";
27 -import { c as _c } from "react/compiler-runtime"; // @instrumentForget @compilationMode(annotation)
27 +import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
28
29 function Bar(props) {
30 "use forget";
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-instrument-forget-test.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @instrumentForget @compilationMode(annotation)
1 +// @enableEmitInstrumentForget @compilationMode(annotation)
2
3 function Bar(props) {
4 'use forget';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.expect.md
+2 -2
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @enableInlineJsxTransform
5 +// @inlineJsxTransform
6
7 function Parent({children, a: _a, b: _b, c: _c, ref}) {
8 return <div ref={ref}>{children}</div>;
@@ -76,7 +76,7 @@ export const FIXTURE_ENTRYPOINT = {
76 ## Code
77
78 ```javascript
79 -import { c as _c2 } from "react/compiler-runtime"; // @enableInlineJsxTransform
79 +import { c as _c2 } from "react/compiler-runtime"; // @inlineJsxTransform
80
81 function Parent(t0) {
82 const $ = _c2(2);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inline-jsx-transform.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @enableInlineJsxTransform
1 +// @inlineJsxTransform
2
3 function Parent({children, a: _a, b: _b, c: _c, ref}) {
4 return <div ref={ref}>{children}</div>;
compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts
+3 -3
@@ -5,9 +5,9 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {parseConfigPragma, validateEnvironmentConfig} from '..';
8 +import {parseConfigPragmaForTests, validateEnvironmentConfig} from '..';
9
10 -describe('parseConfigPragma()', () => {
10 +describe('parseConfigPragmaForTests()', () => {
11 it('parses flags in various forms', () => {
12 const defaultConfig = validateEnvironmentConfig({});
13
@@ -17,7 +17,7 @@ describe('parseConfigPragma()', () => {
17 expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false);
18 expect(defaultConfig.validateNoSetStateInRender).toBe(true);
19
20 - const config = parseConfigPragma(
20 + const config = parseConfigPragmaForTests(
21 '@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false',
22 );
23 expect(config).toEqual({
compiler/packages/babel-plugin-react-compiler/src/index.ts
+1 -1
@@ -26,7 +26,7 @@ export {
26 export {
27 Effect,
28 ValueKind,
29 - parseConfigPragma,
29 + parseConfigPragmaForTests,
30 printHIR,
31 validateEnvironmentConfig,
32 type EnvironmentConfig,
compiler/packages/snap/src/compiler.ts
+14 -60
@@ -21,10 +21,9 @@ import type {
21 } from 'babel-plugin-react-compiler/src/Entrypoint';
22 import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR';
23 import type {
24 - EnvironmentConfig,
24 Macro,
25 MacroMethod,
27 - parseConfigPragma as ParseConfigPragma,
26 + parseConfigPragmaForTests as ParseConfigPragma,
27 } from 'babel-plugin-react-compiler/src/HIR/Environment';
28 import * as HermesParser from 'hermes-parser';
29 import invariant from 'invariant';
@@ -37,6 +36,11 @@ export function parseLanguage(source: string): 'flow' | 'typescript' {
36 return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';
37 }
38
39 +/**
40 + * Parse react compiler plugin + environment options from test fixture. Note
41 + * that although this primarily uses `Environment:parseConfigPragma`, it also
42 + * has test fixture specific (i.e. not applicable to playground) parsing logic.
43 + */
44 function makePluginOptions(
45 firstLine: string,
46 parseConfigPragmaFn: typeof ParseConfigPragma,
@@ -44,15 +48,11 @@ function makePluginOptions(
48 ValueKindEnum: typeof ValueKind,
49 ): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] {
50 let gating = null;
47 - let enableEmitInstrumentForget = null;
48 - let enableEmitFreeze = null;
49 - let enableEmitHookGuards = null;
51 let compilationMode: CompilationMode = 'all';
52 let panicThreshold: PanicThresholdOptions = 'all_errors';
53 let hookPattern: string | null = null;
54 // TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false
55 let validatePreserveExistingMemoizationGuarantees = false;
55 - let enableChangeDetectionForDebugging = null;
56 let customMacros: null | Array<Macro> = null;
57 let validateBlocklistedImports = null;
58 let target = '19' as const;
@@ -78,31 +78,6 @@ function makePluginOptions(
78 importSpecifierName: 'isForgetEnabled_Fixtures',
79 };
80 }
81 - if (firstLine.includes('@instrumentForget')) {
82 - enableEmitInstrumentForget = {
83 - fn: {
84 - source: 'react-compiler-runtime',
85 - importSpecifierName: 'useRenderCounter',
86 - },
87 - gating: {
88 - source: 'react-compiler-runtime',
89 - importSpecifierName: 'shouldInstrument',
90 - },
91 - globalGating: '__DEV__',
92 - };
93 - }
94 - if (firstLine.includes('@enableEmitFreeze')) {
95 - enableEmitFreeze = {
96 - source: 'react-compiler-runtime',
97 - importSpecifierName: 'makeReadOnly',
98 - };
99 - }
100 - if (firstLine.includes('@enableEmitHookGuards')) {
101 - enableEmitHookGuards = {
102 - source: 'react-compiler-runtime',
103 - importSpecifierName: '$dispatcherGuard',
104 - };
105 - }
81
82 const targetMatch = /@target="([^"]+)"/.exec(firstLine);
83 if (targetMatch) {
@@ -132,16 +107,18 @@ function makePluginOptions(
107 ignoreUseNoForget = true;
108 }
109
110 + /**
111 + * Snap currently runs all fixtures without `validatePreserveExistingMemo` as
112 + * most fixtures are interested in compilation output, not whether the
113 + * compiler was able to preserve existing memo.
114 + *
115 + * TODO: flip the default. `useMemo` is rare in test fixtures -- fixtures that
116 + * use useMemo should be explicit about whether this flag is enabled
117 + */
118 if (firstLine.includes('@validatePreserveExistingMemoizationGuarantees')) {
119 validatePreserveExistingMemoizationGuarantees = true;
120 }
121
139 - if (firstLine.includes('@enableChangeDetectionForDebugging')) {
140 - enableChangeDetectionForDebugging = {
141 - source: 'react-compiler-runtime',
142 - importSpecifierName: '$structuralCheck',
143 - };
144 - }
122 const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
123 if (
124 hookPatternMatch &&
@@ -197,22 +174,6 @@ function makePluginOptions(
174 .filter(s => s.length > 0);
175 }
176
200 - let lowerContextAccess = null;
201 - if (firstLine.includes('@lowerContextAccess')) {
202 - lowerContextAccess = {
203 - source: 'react-compiler-runtime',
204 - importSpecifierName: 'useContext_withSelector',
205 - };
206 - }
207 -
208 - let inlineJsxTransform: EnvironmentConfig['inlineJsxTransform'] = null;
209 - if (firstLine.includes('@enableInlineJsxTransform')) {
210 - inlineJsxTransform = {
211 - elementSymbol: 'react.transitional.element',
212 - globalDevVar: 'DEV',
213 - };
214 - }
215 -
177 let logs: Array<{filename: string | null; event: LoggerEvent}> = [];
178 let logger: Logger | null = null;
179 if (firstLine.includes('@logger')) {
@@ -232,17 +193,10 @@ function makePluginOptions(
193 ValueKindEnum,
194 }),
195 customMacros,
235 - enableEmitFreeze,
236 - enableEmitInstrumentForget,
237 - enableEmitHookGuards,
196 assertValidMutableRanges: true,
239 - enableSharedRuntime__testonly: true,
197 hookPattern,
198 validatePreserveExistingMemoizationGuarantees,
242 - enableChangeDetectionForDebugging,
243 - lowerContextAccess,
199 validateBlocklistedImports,
245 - inlineJsxTransform,
200 },
201 compilationMode,
202 logger,
compiler/packages/snap/src/runner-worker.ts
+4 -4
@@ -7,7 +7,7 @@
7
8 import {codeFrameColumns} from '@babel/code-frame';
9 import type {PluginObj} from '@babel/core';
10 -import type {parseConfigPragma as ParseConfigPragma} from 'babel-plugin-react-compiler/src/HIR/Environment';
10 +import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/HIR/Environment';
11 import {TransformResult, transformFixtureInput} from './compiler';
12 import {
13 COMPILER_PATH,
@@ -65,8 +65,8 @@ async function compile(
65 COMPILER_INDEX_PATH,
66 );
67 const {toggleLogging} = require(LOGGER_PATH);
68 - const {parseConfigPragma} = require(PARSE_CONFIG_PRAGMA_PATH) as {
69 - parseConfigPragma: typeof ParseConfigPragma;
68 + const {parseConfigPragmaForTests} = require(PARSE_CONFIG_PRAGMA_PATH) as {
69 + parseConfigPragmaForTests: typeof ParseConfigPragma;
70 };
71
72 // only try logging if we filtered out all but one fixture,
@@ -75,7 +75,7 @@ async function compile(
75 const result = await transformFixtureInput(
76 input,
77 fixturePath,
78 - parseConfigPragma,
78 + parseConfigPragmaForTests,
79 BabelPluginReactCompiler,
80 includeEvaluator,
81 EffectEnum,