Skip to content

Commit

Permalink
[compiler] Run compiler pipeline on 'use no forget'
Browse files Browse the repository at this point in the history
This PR updates the babel plugin to continue the compilation pipeline as
normal on components/hooks that have been opted out using a directive.
Instead, we no longer emit the compiled function when the directive is
present.

Previously, we would skip over the entire pipeline. By continuing to
enter the pipeline, we'll be able to detect if there are unused
directives.

The end result is:

- (no change) 'use forget' will always opt into compilation
- (new) 'use no forget' will opt out of compilation but continue to log
  errors without throwing them. This means that a Program containing
multiple functions (some of which are opted out) will continue to
compile correctly

ghstack-source-id: 5bd85df2f81350cb2c1998a8761b8ed3fec32a40
Pull Request resolved: #30720
  • Loading branch information
poteto committed Aug 16, 2024
1 parent 7b41cdc commit e9a869f
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ export type LoggerEvent =
fnLoc: t.SourceLocation | null;
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
}
| {
kind: 'CompileSkip';
fnLoc: t.SourceLocation | null;
reason: string;
loc: t.SourceLocation | null;
}
| {
kind: 'CompileSuccess';
fnLoc: t.SourceLocation | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,23 @@ export type CompilerPass = {
comments: Array<t.CommentBlock | t.CommentLine>;
code: string | null;
};
const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);

function findDirectiveEnablingMemoization(
directives: Array<t.Directive>,
): t.Directive | null {
for (const directive of directives) {
const directiveValue = directive.value.value;
if (directiveValue === 'use forget' || directiveValue === 'use memo') {
return directive;
}
}
return null;
): Array<t.Directive> {
return directives.filter(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
);
}

function findDirectiveDisablingMemoization(
directives: Array<t.Directive>,
options: PluginOptions,
): t.Directive | null {
for (const directive of directives) {
const directiveValue = directive.value.value;
if (
(directiveValue === 'use no forget' ||
directiveValue === 'use no memo') &&
!options.ignoreUseNoForget
) {
return directive;
}
}
return null;
): Array<t.Directive> {
return directives.filter(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
);
}

function isCriticalError(err: unknown): boolean {
Expand Down Expand Up @@ -102,7 +91,7 @@ export type CompileResult = {
compiledFn: CodegenFunction;
};

function handleError(
function logError(
err: unknown,
pass: CompilerPass,
fnLoc: t.SourceLocation | null,
Expand Down Expand Up @@ -131,6 +120,13 @@ function handleError(
});
}
}
}
function handleError(
err: unknown,
pass: CompilerPass,
fnLoc: t.SourceLocation | null,
): void {
logError(err, pass, fnLoc);
if (
pass.opts.panicThreshold === 'all_errors' ||
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
Expand Down Expand Up @@ -393,6 +389,17 @@ export function compileProgram(
fn: BabelFn,
fnType: ReactFunctionType,
): null | CodegenFunction => {
let optInDirectives: Array<t.Directive> = [];
let optOutDirectives: Array<t.Directive> = [];
if (fn.node.body.type === 'BlockStatement') {
optInDirectives = findDirectiveEnablingMemoization(
fn.node.body.directives,
);
optOutDirectives = findDirectiveDisablingMemoization(
fn.node.body.directives,
);
}

if (lintError != null) {
/**
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
Expand All @@ -404,7 +411,11 @@ export function compileProgram(
fn,
);
if (suppressionsInFunction.length > 0) {
handleError(lintError, pass, fn.node.loc ?? null);
if (optOutDirectives.length > 0) {
logError(lintError, pass, fn.node.loc ?? null);
} else {
handleError(lintError, pass, fn.node.loc ?? null);
}
}
}

Expand All @@ -430,11 +441,50 @@ export function compileProgram(
prunedMemoValues: compiledFn.prunedMemoValues,
});
} catch (err) {
/**
* If an opt out directive is present, log only instead of throwing and don't mark as
* containing a critical error.
*/
if (fn.node.body.type === 'BlockStatement') {
if (optOutDirectives.length > 0) {
logError(err, pass, fn.node.loc ?? null);
return null;
}
}
hasCriticalError ||= isCriticalError(err);
handleError(err, pass, fn.node.loc ?? null);
return null;
}

/**
* Always compile functions with opt in directives.
*/
if (optInDirectives.length > 0) {
return compiledFn;
} else if (pass.opts.compilationMode === 'annotation') {
/**
* No opt-in directive in annotation mode, so don't insert the compiled function.
*/
return null;
}

/**
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
* unused 'use no forget/memo' directive.
*/
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
for (const directive of optOutDirectives) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileSkip',
fnLoc: fn.node.body.loc ?? null,
reason: `Skipped due to '${directive.value.value}' directive.`,
loc: directive.loc ?? null,
});
}
return null;
}

if (!pass.opts.noEmit && !hasCriticalError) {
return compiledFn;
}
Expand Down Expand Up @@ -481,6 +531,16 @@ export function compileProgram(
});
}

/**
* Do not modify source if there is a module scope level opt out directive.
*/
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
program.node.directives,
);
if (moduleScopeOptOutDirectives.length > 0) {
return;
}

if (pass.opts.gating != null) {
const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
program,
Expand Down Expand Up @@ -596,24 +656,6 @@ function shouldSkipCompilation(
}
}

// Top level "use no forget", skip this file entirely
const useNoForget = findDirectiveDisablingMemoization(
program.node.directives,
pass.opts,
);
if (useNoForget != null) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileError',
fnLoc: null,
detail: {
severity: ErrorSeverity.Todo,
reason: 'Skipped due to "use no forget" directive.',
loc: useNoForget.loc ?? null,
suggestions: null,
},
});
return true;
}
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
if (hasMemoCacheFunctionImport(program, moduleName)) {
return true;
Expand All @@ -631,28 +673,8 @@ function getReactFunctionType(
): ReactFunctionType | null {
const hookPattern = environment.hookPattern;
if (fn.node.body.type === 'BlockStatement') {
// Opt-outs disable compilation regardless of mode
const useNoForget = findDirectiveDisablingMemoization(
fn.node.body.directives,
pass.opts,
);
if (useNoForget != null) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileError',
fnLoc: fn.node.body.loc ?? null,
detail: {
severity: ErrorSeverity.Todo,
reason: 'Skipped due to "use no forget" directive.',
loc: useNoForget.loc ?? null,
suggestions: null,
},
});
return null;
}
// Otherwise opt-ins enable compilation regardless of mode
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) {
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
}
}

// Component and hook declarations are known components/hooks
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

## Input

```javascript
function Component() {
'use no forget';
return <div>Hello World</div>;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: true,
};

```

## Code

```javascript
function Component() {
"use no forget";
return <div>Hello World</div>;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: true,
};

```
### Eval output
(kind: ok) <div>Hello World</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function Component() {
'use no forget';
return <div>Hello World</div>;
}

export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [],
isComponent: true,
};
1 change: 1 addition & 0 deletions compiler/packages/babel-plugin-react-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
compileProgram,
parsePluginOptions,
run,
OPT_OUT_DIRECTIVES,
type CompilerPipelineValue,
type PluginOptions,
} from './Entrypoint';
Expand Down

0 comments on commit e9a869f

Please sign in to comment.