diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 5ac580abbf0..a653dd37360 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -520,6 +520,9 @@ function findFunctionsToCompile( programContext: ProgramContext, ): Array { const queue: Array = []; + // Track functions that have 'use no memo' to skip their nested functions + const optedOutFunctions: Set = new Set(); + const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => { // In 'all' mode, compile only top level functions if ( @@ -529,6 +532,18 @@ function findFunctionsToCompile( return; } + // Check if this function has 'use no memo' directive and track it + if (fn.node.body.type === 'BlockStatement') { + const optOut = findDirectiveDisablingMemoization( + fn.node.body.directives, + pass.opts, + ); + if (optOut != null) { + // Track this function so nested functions are also skipped + optedOutFunctions.add(fn.node); + } + } + const fnType = getReactFunctionType(fn, pass); if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) { @@ -539,6 +554,34 @@ function findFunctionsToCompile( return; } + // Check if this function explicitly opts in with 'use memo' + let hasExplicitOptIn = false; + if (fn.node.body.type === 'BlockStatement') { + const optIn = tryFindDirectiveEnablingMemoization( + fn.node.body.directives, + pass.opts, + ); + hasExplicitOptIn = optIn.isOk() && optIn.unwrap() != null; + } + + // Skip if any ancestor function has 'use no memo', unless this function explicitly opts in + if (!hasExplicitOptIn) { + let parentPath: NodePath | null = fn.parentPath; + while (parentPath != null) { + if ( + parentPath.isFunctionDeclaration() || + parentPath.isFunctionExpression() || + parentPath.isArrowFunctionExpression() + ) { + if (optedOutFunctions.has(parentPath.node)) { + // Parent has 'use no memo', skip this nested function + return; + } + } + parentPath = parentPath.parentPath; + } + } + /* * We may be generating a new FunctionDeclaration node, so we must skip over it or this * traversal will loop infinitely. diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-deeply-nested.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-deeply-nested.expect.md new file mode 100644 index 00000000000..04facc66181 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-deeply-nested.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +// @compilationMode(infer) + +/** + * Test that deeply nested functions are NOT compiled when top-level parent has 'use no memo'. + */ +function ParentComponent(props) { + 'use no memo'; + + function Level1() { + function Level2() { + return
{props.value}
; + } + return Level2; + } + + return props.render(Level1()); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test', render: C => C()}], + isComponent: true, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) + +/** + * Test that deeply nested functions are NOT compiled when top-level parent has 'use no memo'. + */ +function ParentComponent(props) { + "use no memo"; + + function Level1() { + function Level2() { + return
{props.value}
; + } + return Level2; + } + + return props.render(Level1()); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [ + { + value: "test", + render: (C) => { + const $ = _c(2); + let t0; + if ($[0] !== C) { + t0 = C(); + $[0] = C; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; + }, + }, + ], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok)
test
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-deeply-nested.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-deeply-nested.js new file mode 100644 index 00000000000..d101016c65a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-deeply-nested.js @@ -0,0 +1,23 @@ +// @compilationMode(infer) + +/** + * Test that deeply nested functions are NOT compiled when top-level parent has 'use no memo'. + */ +function ParentComponent(props) { + 'use no memo'; + + function Level1() { + function Level2() { + return
{props.value}
; + } + return Level2; + } + + return props.render(Level1()); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test', render: C => C()}], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-arrow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-arrow.expect.md new file mode 100644 index 00000000000..2dfb6d7c4f2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-arrow.expect.md @@ -0,0 +1,73 @@ + +## Input + +```javascript +// @compilationMode(infer) + +/** + * Test that nested arrow functions with component-like names are NOT compiled + * when parent has 'use no memo'. + */ +function ParentComponent(props) { + 'use no memo'; + + const NestedComponent = () => { + return
{props.value}
; + }; + + return props.render(NestedComponent); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test', render: C => C()}], + isComponent: true, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) + +/** + * Test that nested arrow functions with component-like names are NOT compiled + * when parent has 'use no memo'. + */ +function ParentComponent(props) { + "use no memo"; + + const NestedComponent = () => { + return
{props.value}
; + }; + + return props.render(NestedComponent); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [ + { + value: "test", + render: (C) => { + const $ = _c(2); + let t0; + if ($[0] !== C) { + t0 = C(); + $[0] = C; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; + }, + }, + ], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok)
test
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-arrow.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-arrow.js new file mode 100644 index 00000000000..36940a21d55 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-arrow.js @@ -0,0 +1,21 @@ +// @compilationMode(infer) + +/** + * Test that nested arrow functions with component-like names are NOT compiled + * when parent has 'use no memo'. + */ +function ParentComponent(props) { + 'use no memo'; + + const NestedComponent = () => { + return
{props.value}
; + }; + + return props.render(NestedComponent); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test', render: C => C()}], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-component.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-component.expect.md new file mode 100644 index 00000000000..6c2d57087d2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-component.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +// @compilationMode(infer) + +/** + * Test that nested component-like functions are NOT compiled when parent has 'use no memo'. + * This reproduces bug #35350 where 'use no memo' doesn't apply recursively. + */ +function ParentComponent(props) { + 'use no memo'; + + // This nested function has a component-like name but should NOT be compiled + // because the parent has 'use no memo' + function NestedComponent() { + return
{props.value}
; + } + + return props.render(NestedComponent); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test', render: C => C()}], + isComponent: true, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) + +/** + * Test that nested component-like functions are NOT compiled when parent has 'use no memo'. + * This reproduces bug #35350 where 'use no memo' doesn't apply recursively. + */ +function ParentComponent(props) { + "use no memo"; + + // This nested function has a component-like name but should NOT be compiled + // because the parent has 'use no memo' + function NestedComponent() { + return
{props.value}
; + } + + return props.render(NestedComponent); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [ + { + value: "test", + render: (C) => { + const $ = _c(2); + let t0; + if ($[0] !== C) { + t0 = C(); + $[0] = C; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; + }, + }, + ], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok)
test
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-component.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-component.js new file mode 100644 index 00000000000..3253936a32c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-component.js @@ -0,0 +1,23 @@ +// @compilationMode(infer) + +/** + * Test that nested component-like functions are NOT compiled when parent has 'use no memo'. + * This reproduces bug #35350 where 'use no memo' doesn't apply recursively. + */ +function ParentComponent(props) { + 'use no memo'; + + // This nested function has a component-like name but should NOT be compiled + // because the parent has 'use no memo' + function NestedComponent() { + return
{props.value}
; + } + + return props.render(NestedComponent); +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test', render: C => C()}], + isComponent: true, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-with-opt-in.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-with-opt-in.expect.md new file mode 100644 index 00000000000..2e7af1a2319 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-with-opt-in.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +// @compilationMode(infer) + +/** + * Test that explicit 'use memo' in nested function overrides parent's 'use no memo'. + * The nested function SHOULD be compiled because it explicitly opts in. + */ +function ParentComponent(props) { + 'use no memo'; + + // This should still be compiled because it explicitly opts in + function NestedComponent() { + 'use memo'; + return
{props.value}
; + } + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test'}], + isComponent: true, +}; + +``` + +## Code + +```javascript +// @compilationMode(infer) + +/** + * Test that explicit 'use memo' in nested function overrides parent's 'use no memo'. + * The nested function SHOULD be compiled because it explicitly opts in. + */ +function ParentComponent(props) { + "use no memo"; + + // This should still be compiled because it explicitly opts in + function NestedComponent() { + "use memo"; + return
{props.value}
; + } + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{ value: "test" }], + isComponent: true, +}; + +``` + +### Eval output +(kind: ok)
test
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-with-opt-in.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-with-opt-in.js new file mode 100644 index 00000000000..99e31460d49 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-no-memo-nested-with-opt-in.js @@ -0,0 +1,23 @@ +// @compilationMode(infer) + +/** + * Test that explicit 'use memo' in nested function overrides parent's 'use no memo'. + * The nested function SHOULD be compiled because it explicitly opts in. + */ +function ParentComponent(props) { + 'use no memo'; + + // This should still be compiled because it explicitly opts in + function NestedComponent() { + 'use memo'; + return
{props.value}
; + } + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: ParentComponent, + params: [{value: 'test'}], + isComponent: true, +};