Skip to content

Commit 2758f27

Browse files
committed
Update on "[compiler] Validate environment config while parsing plugin opts"
Addresses a todo from a while back. We now validate environment options when parsing the plugin options, which means we can stop re-parsing/validating in later phases. [ghstack-poisoned]
2 parents 7b026a7 + 6494620 commit 2758f27

39 files changed

Lines changed: 1135 additions & 494 deletions

File tree

ReactVersions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const stablePackages = {
5252
// These packages do not exist in the @canary or @latest channel, only
5353
// @experimental. We don't use semver, just the commit sha, so this is just a
5454
// list of package names instead of a map.
55-
const experimentalPackages = [];
55+
const experimentalPackages = ['react-markup'];
5656

5757
module.exports = {
5858
ReactVersion,

compiler/apps/playground/components/Editor/EditorImpl.tsx

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ function parseFunctions(
6666
source: string,
6767
language: 'flow' | 'typescript',
6868
): Array<
69-
NodePath<
70-
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
71-
>
69+
| NodePath<t.FunctionDeclaration>
70+
| NodePath<t.ArrowFunctionExpression>
71+
| NodePath<t.FunctionExpression>
7272
> {
7373
const items: Array<
74-
NodePath<
75-
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
76-
>
74+
| NodePath<t.FunctionDeclaration>
75+
| NodePath<t.ArrowFunctionExpression>
76+
| NodePath<t.FunctionExpression>
7777
> = [];
7878
try {
7979
const ast = parseInput(source, language);
@@ -155,22 +155,33 @@ function isHookName(s: string): boolean {
155155
return /^use[A-Z0-9]/.test(s);
156156
}
157157

158-
function getReactFunctionType(
159-
id: NodePath<t.Identifier | null | undefined>,
160-
): ReactFunctionType {
161-
if (id && id.node && id.isIdentifier()) {
162-
if (isHookName(id.node.name)) {
158+
function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
159+
if (id != null) {
160+
if (isHookName(id.name)) {
163161
return 'Hook';
164162
}
165163

166164
const isPascalCaseNameSpace = /^[A-Z].*/;
167-
if (isPascalCaseNameSpace.test(id.node.name)) {
165+
if (isPascalCaseNameSpace.test(id.name)) {
168166
return 'Component';
169167
}
170168
}
171169
return 'Other';
172170
}
173171

172+
function getFunctionIdentifier(
173+
fn:
174+
| NodePath<t.FunctionDeclaration>
175+
| NodePath<t.ArrowFunctionExpression>
176+
| NodePath<t.FunctionExpression>,
177+
): t.Identifier | null {
178+
if (fn.isArrowFunctionExpression()) {
179+
return null;
180+
}
181+
const id = fn.get('id');
182+
return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
183+
}
184+
174185
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
175186
const results = new Map<string, PrintedCompilerPipelineValue[]>();
176187
const error = new CompilerError();
@@ -188,27 +199,21 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
188199
} else {
189200
language = 'typescript';
190201
}
202+
let count = 0;
203+
const withIdentifier = (id: t.Identifier | null): t.Identifier => {
204+
if (id != null && id.name != null) {
205+
return id;
206+
} else {
207+
return t.identifier(`anonymous_${count++}`);
208+
}
209+
};
191210
try {
192211
// Extract the first line to quickly check for custom test directives
193212
const pragma = source.substring(0, source.indexOf('\n'));
194213
const config = parseConfigPragma(pragma);
195214

196215
for (const fn of parseFunctions(source, language)) {
197-
if (!fn.isFunctionDeclaration()) {
198-
error.pushErrorDetail(
199-
new CompilerErrorDetail({
200-
reason: `Unexpected function type ${fn.node.type}`,
201-
description:
202-
'Playground only supports parsing function declarations',
203-
severity: ErrorSeverity.Todo,
204-
loc: fn.node.loc ?? null,
205-
suggestions: null,
206-
}),
207-
);
208-
continue;
209-
}
210-
211-
const id = fn.get('id');
216+
const id = withIdentifier(getFunctionIdentifier(fn));
212217
for (const result of run(
213218
fn,
214219
{
@@ -221,7 +226,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
221226
null,
222227
null,
223228
)) {
224-
const fnName = fn.node.id?.name ?? null;
229+
const fnName = id.name;
225230
switch (result.kind) {
226231
case 'ast': {
227232
upsert({
@@ -230,7 +235,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
230235
name: result.name,
231236
value: {
232237
type: 'FunctionDeclaration',
233-
id: result.value.id,
238+
id,
234239
async: result.value.async,
235240
generator: result.value.generator,
236241
body: result.value.body,

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ export type LoggerEvent =
169169
fnLoc: t.SourceLocation | null;
170170
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
171171
}
172+
| {
173+
kind: 'CompileSkip';
174+
fnLoc: t.SourceLocation | null;
175+
reason: string;
176+
loc: t.SourceLocation | null;
177+
}
172178
| {
173179
kind: 'CompileSuccess';
174180
fnLoc: t.SourceLocation | null;

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ import {outlineFunctions} from '../Optimization/OutlineFunctions';
105105
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
106106
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
107107
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
108-
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
109108

110109
export type CompilerPipelineValue =
111110
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -250,10 +249,6 @@ function* runWithEnvironment(
250249
validateNoSetStateInPassiveEffects(hir);
251250
}
252251

253-
if (env.config.validateNoJSXInTryStatements) {
254-
validateNoJSXInTryStatement(hir);
255-
}
256-
257252
inferReactivePlaces(hir);
258253
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
259254

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 84 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -42,34 +42,23 @@ export type CompilerPass = {
4242
comments: Array<t.CommentBlock | t.CommentLine>;
4343
code: string | null;
4444
};
45+
const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
46+
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
4547

4648
function findDirectiveEnablingMemoization(
4749
directives: Array<t.Directive>,
48-
): t.Directive | null {
49-
for (const directive of directives) {
50-
const directiveValue = directive.value.value;
51-
if (directiveValue === 'use forget' || directiveValue === 'use memo') {
52-
return directive;
53-
}
54-
}
55-
return null;
50+
): Array<t.Directive> {
51+
return directives.filter(directive =>
52+
OPT_IN_DIRECTIVES.has(directive.value.value),
53+
);
5654
}
5755

5856
function findDirectiveDisablingMemoization(
5957
directives: Array<t.Directive>,
60-
options: PluginOptions,
61-
): t.Directive | null {
62-
for (const directive of directives) {
63-
const directiveValue = directive.value.value;
64-
if (
65-
(directiveValue === 'use no forget' ||
66-
directiveValue === 'use no memo') &&
67-
!options.ignoreUseNoForget
68-
) {
69-
return directive;
70-
}
71-
}
72-
return null;
58+
): Array<t.Directive> {
59+
return directives.filter(directive =>
60+
OPT_OUT_DIRECTIVES.has(directive.value.value),
61+
);
7362
}
7463

7564
function isCriticalError(err: unknown): boolean {
@@ -101,7 +90,7 @@ export type CompileResult = {
10190
compiledFn: CodegenFunction;
10291
};
10392

104-
function handleError(
93+
function logError(
10594
err: unknown,
10695
pass: CompilerPass,
10796
fnLoc: t.SourceLocation | null,
@@ -130,6 +119,13 @@ function handleError(
130119
});
131120
}
132121
}
122+
}
123+
function handleError(
124+
err: unknown,
125+
pass: CompilerPass,
126+
fnLoc: t.SourceLocation | null,
127+
): void {
128+
logError(err, pass, fnLoc);
133129
if (
134130
pass.opts.panicThreshold === 'all_errors' ||
135131
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
@@ -378,6 +374,17 @@ export function compileProgram(
378374
fn: BabelFn,
379375
fnType: ReactFunctionType,
380376
): null | CodegenFunction => {
377+
let optInDirectives: Array<t.Directive> = [];
378+
let optOutDirectives: Array<t.Directive> = [];
379+
if (fn.node.body.type === 'BlockStatement') {
380+
optInDirectives = findDirectiveEnablingMemoization(
381+
fn.node.body.directives,
382+
);
383+
optOutDirectives = findDirectiveDisablingMemoization(
384+
fn.node.body.directives,
385+
);
386+
}
387+
381388
if (lintError != null) {
382389
/**
383390
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
@@ -389,7 +396,11 @@ export function compileProgram(
389396
fn,
390397
);
391398
if (suppressionsInFunction.length > 0) {
392-
handleError(lintError, pass, fn.node.loc ?? null);
399+
if (optOutDirectives.length > 0) {
400+
logError(lintError, pass, fn.node.loc ?? null);
401+
} else {
402+
handleError(lintError, pass, fn.node.loc ?? null);
403+
}
393404
}
394405
}
395406

@@ -415,11 +426,50 @@ export function compileProgram(
415426
prunedMemoValues: compiledFn.prunedMemoValues,
416427
});
417428
} catch (err) {
429+
/**
430+
* If an opt out directive is present, log only instead of throwing and don't mark as
431+
* containing a critical error.
432+
*/
433+
if (fn.node.body.type === 'BlockStatement') {
434+
if (optOutDirectives.length > 0) {
435+
logError(err, pass, fn.node.loc ?? null);
436+
return null;
437+
}
438+
}
418439
hasCriticalError ||= isCriticalError(err);
419440
handleError(err, pass, fn.node.loc ?? null);
420441
return null;
421442
}
422443

444+
/**
445+
* Always compile functions with opt in directives.
446+
*/
447+
if (optInDirectives.length > 0) {
448+
return compiledFn;
449+
} else if (pass.opts.compilationMode === 'annotation') {
450+
/**
451+
* No opt-in directive in annotation mode, so don't insert the compiled function.
452+
*/
453+
return null;
454+
}
455+
456+
/**
457+
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
458+
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
459+
* unused 'use no forget/memo' directive.
460+
*/
461+
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
462+
for (const directive of optOutDirectives) {
463+
pass.opts.logger?.logEvent(pass.filename, {
464+
kind: 'CompileSkip',
465+
fnLoc: fn.node.body.loc ?? null,
466+
reason: `Skipped due to '${directive.value.value}' directive.`,
467+
loc: directive.loc ?? null,
468+
});
469+
}
470+
return null;
471+
}
472+
423473
if (!pass.opts.noEmit && !hasCriticalError) {
424474
return compiledFn;
425475
}
@@ -466,6 +516,16 @@ export function compileProgram(
466516
});
467517
}
468518

519+
/**
520+
* Do not modify source if there is a module scope level opt out directive.
521+
*/
522+
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
523+
program.node.directives,
524+
);
525+
if (moduleScopeOptOutDirectives.length > 0) {
526+
return;
527+
}
528+
469529
if (pass.opts.gating != null) {
470530
const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
471531
program,
@@ -581,24 +641,6 @@ function shouldSkipCompilation(
581641
}
582642
}
583643

584-
// Top level "use no forget", skip this file entirely
585-
const useNoForget = findDirectiveDisablingMemoization(
586-
program.node.directives,
587-
pass.opts,
588-
);
589-
if (useNoForget != null) {
590-
pass.opts.logger?.logEvent(pass.filename, {
591-
kind: 'CompileError',
592-
fnLoc: null,
593-
detail: {
594-
severity: ErrorSeverity.Todo,
595-
reason: 'Skipped due to "use no forget" directive.',
596-
loc: useNoForget.loc ?? null,
597-
suggestions: null,
598-
},
599-
});
600-
return true;
601-
}
602644
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
603645
if (hasMemoCacheFunctionImport(program, moduleName)) {
604646
return true;
@@ -616,28 +658,8 @@ function getReactFunctionType(
616658
): ReactFunctionType | null {
617659
const hookPattern = environment.hookPattern;
618660
if (fn.node.body.type === 'BlockStatement') {
619-
// Opt-outs disable compilation regardless of mode
620-
const useNoForget = findDirectiveDisablingMemoization(
621-
fn.node.body.directives,
622-
pass.opts,
623-
);
624-
if (useNoForget != null) {
625-
pass.opts.logger?.logEvent(pass.filename, {
626-
kind: 'CompileError',
627-
fnLoc: fn.node.body.loc ?? null,
628-
detail: {
629-
severity: ErrorSeverity.Todo,
630-
reason: 'Skipped due to "use no forget" directive.',
631-
loc: useNoForget.loc ?? null,
632-
suggestions: null,
633-
},
634-
});
635-
return null;
636-
}
637-
// Otherwise opt-ins enable compilation regardless of mode
638-
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) {
661+
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
639662
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
640-
}
641663
}
642664

643665
// Component and hook declarations are known components/hooks

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,6 @@ const EnvironmentConfigSchema = z.object({
237237
*/
238238
validateNoSetStateInPassiveEffects: z.boolean().default(false),
239239

240-
/**
241-
* Validates against creating JSX within a try block and recommends using an error boundary
242-
* instead.
243-
*/
244-
validateNoJSXInTryStatements: z.boolean().default(false),
245-
246240
/**
247241
* Validates that the dependencies of all effect hooks are memoized. This helps ensure
248242
* that Forget does not introduce infinite renders caused by a dependency changing,

0 commit comments

Comments
 (0)