@samitouri / QOS-React / commits / a9410fb487

[compiler] Option to infer names for anonymous functions (#34410)

Adds a `@enableNameAnonymousFunctions` feature to infer helpful names for anonymous functions within components and hooks. The logic is inspired by a custom Next.js transform, flagged to us by @eps1lon, that does something similar. Implementing this transform within React Compiler means that all React (Compiler) users can benefit from more helpful names when debugging. The idea builds on the fact that JS engines try to infer helpful names for anonymous functions (in stack traces) when those functions are accessed through an object property lookup: ```js ({'a[xyz]': () => { throw new Error('hello!') } }['a[xyz]'])() // Stack trace: Uncaught Error: hello! at a[xyz] (<anonymous>:1:26) // <-- note the name here at <anonymous>:1:60 ``` The new NameAnonymousFunctions transform is gated by the above flag, which is off by default. It attemps to infer names for functions as follows: First, determine a "local" name: * Assigning a function to a named variable uses the variable name. `const f = () => {}` gets the name "f". * Passing the function as an argument to a function gets the name of the function, ie `foo(() => ...)` get the name "foo()", `foo.bar(() => ...)` gets the name "foo.bar()". Note the parenthesis to help understand that it was part of a call. * Passing the function to a known hook uses the name of the hook, `useEffect(() => ...)` uses "useEffect()". * Passing the function as a JSX prop uses the element and attr name, eg `<div onClick={() => ...}` uses "<div>.onClick". Second, the local name is combined with the name of the outer component/hook, so the final names will be strings like `Component[f]` or `useMyHook[useEffect()]`. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34410). * #34434 * __->__ #34410

Joseph Savona committed Sep 9, 2025 at 10:22 UTC a9410fb487776339ec68e57a57a570be952ccad0
8 files changed +544 -2
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+10
@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
103 import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
104 import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
105 import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
106 +import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
107
108 export type CompilerPipelineValue =
109 | {kind: 'ast'; name: string; value: CodegenFunction}
@@ -414,6 +415,15 @@ function runWithEnvironment(
415 });
416 }
417
418 + if (env.config.enableNameAnonymousFunctions) {
419 + nameAnonymousFunctions(hir);
420 + log({
421 + kind: 'hir',
422 + name: 'NameAnonymougFunctions',
423 + value: hir,
424 + });
425 + }
426 +
427 const reactiveFunction = buildReactiveFunction(hir);
428 log({
429 kind: 'reactive',
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
+2
@@ -3566,6 +3566,8 @@ function lowerFunctionToValue(
3566 let name: string | null = null;
3567 if (expr.isFunctionExpression()) {
3568 name = expr.get('id')?.node?.name ?? null;
3569 + } else if (expr.isFunctionDeclaration()) {
3570 + name = expr.get('id')?.node?.name ?? null;
3571 }
3572 const loweredFunc = lowerFunction(builder, expr);
3573 if (!loweredFunc) {
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+2
@@ -261,6 +261,8 @@ export const EnvironmentConfigSchema = z.object({
261
262 enableFire: z.boolean().default(false),
263
264 + enableNameAnonymousFunctions: z.boolean().default(false),
265 +
266 /**
267 * Enables inference and auto-insertion of effect dependencies. Takes in an array of
268 * configurable module and import pairs to allow for user-land experimentation. For example,
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+10
@@ -15,6 +15,7 @@ import {Type, makeType} from './Types';
15 import {z} from 'zod';
16 import type {AliasingEffect} from '../Inference/AliasingEffects';
17 import {isReservedWord} from '../Utils/Keyword';
18 +import {Err, Ok, Result} from '../Utils/Result';
19
20 /*
21 * *******************************************************************************************
@@ -1298,6 +1299,15 @@ export function forkTemporaryIdentifier(
1299 };
1300 }
1301
1302 +export function validateIdentifierName(
1303 + name: string,
1304 +): Result<ValidIdentifierName, null> {
1305 + if (isReservedWord(name) || !t.isValidIdentifier(name)) {
1306 + return Err(null);
1307 + }
1308 + return Ok(makeIdentifierName(name).value);
1309 +}
1310 +
1311 /**
1312 * Creates a valid identifier name. This should *not* be used for synthesizing
1313 * identifier names: only call this method for identifier names that appear in the
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+22 -2
@@ -43,6 +43,7 @@ import {
43 ValidIdentifierName,
44 getHookKind,
45 makeIdentifierName,
46 + validateIdentifierName,
47 } from '../HIR/HIR';
48 import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
49 import {eachPatternOperand} from '../HIR/visitors';
@@ -2326,6 +2327,11 @@ function codegenInstructionValue(
2327 ),
2328 reactiveFunction,
2329 ).unwrap();
2330 +
2331 + const validatedName =
2332 + instrValue.name != null
2333 + ? validateIdentifierName(instrValue.name)
2334 + : Err(null);
2335 if (instrValue.type === 'ArrowFunctionExpression') {
2336 let body: t.BlockStatement | t.Expression = fn.body;
2337 if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -2337,14 +2343,28 @@ function codegenInstructionValue(
2343 value = t.arrowFunctionExpression(fn.params, body, fn.async);
2344 } else {
2345 value = t.functionExpression(
2340 - fn.id ??
2341 - (instrValue.name != null ? t.identifier(instrValue.name) : null),
2346 + validatedName
2347 + .map<t.Identifier | null>(name => t.identifier(name))
2348 + .unwrapOr(null),
2349 fn.params,
2350 fn.body,
2351 fn.generator,
2352 fn.async,
2353 );
2354 }
2355 + if (
2356 + cx.env.config.enableNameAnonymousFunctions &&
2357 + validatedName.isErr() &&
2358 + instrValue.name != null
2359 + ) {
2360 + const name = instrValue.name;
2361 + value = t.memberExpression(
2362 + t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
2363 + t.stringLiteral(name),
2364 + true,
2365 + false,
2366 + );
2367 + }
2368 break;
2369 }
2370 case 'TaggedTemplateExpression': {
compiler/packages/babel-plugin-react-compiler/src/Transform/NameAnonymousFunctions.ts new
+173
@@ -0,0 +1,173 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {
9 + FunctionExpression,
10 + getHookKind,
11 + HIRFunction,
12 + IdentifierId,
13 +} from '../HIR';
14 +
15 +export function nameAnonymousFunctions(fn: HIRFunction): void {
16 + if (fn.id == null) {
17 + return;
18 + }
19 + const parentName = fn.id;
20 + const functions = nameAnonymousFunctionsImpl(fn);
21 + function visit(node: Node, prefix: string): void {
22 + if (node.generatedName != null) {
23 + /**
24 + * Note that we don't generate a name for functions that already had one,
25 + * so we'll only add the prefix to anonymous functions regardless of
26 + * nesting depth.
27 + */
28 + const name = `${prefix}${node.generatedName}]`;
29 + node.fn.name = name;
30 + }
31 + /**
32 + * Whether or not we generated a name for the function at this node,
33 + * traverse into its nested functions to assign them names
34 + */
35 + const nextPrefix = `${prefix}${node.generatedName ?? node.fn.name ?? '<anonymous>'} > `;
36 + for (const inner of node.inner) {
37 + visit(inner, nextPrefix);
38 + }
39 + }
40 + for (const node of functions) {
41 + visit(node, `${parentName}[`);
42 + }
43 +}
44 +
45 +type Node = {
46 + fn: FunctionExpression;
47 + generatedName: string | null;
48 + inner: Array<Node>;
49 +};
50 +
51 +function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
52 + // Functions that we track to generate names for
53 + const functions: Map<IdentifierId, Node> = new Map();
54 + // Tracks temporaries that read from variables/globals/properties
55 + const names: Map<IdentifierId, string> = new Map();
56 + // Tracks all function nodes to bubble up for later renaming
57 + const nodes: Array<Node> = [];
58 + for (const block of fn.body.blocks.values()) {
59 + for (const instr of block.instructions) {
60 + const {lvalue, value} = instr;
61 + switch (value.kind) {
62 + case 'LoadGlobal': {
63 + names.set(lvalue.identifier.id, value.binding.name);
64 + break;
65 + }
66 + case 'LoadContext':
67 + case 'LoadLocal': {
68 + const name = value.place.identifier.name;
69 + if (name != null && name.kind === 'named') {
70 + names.set(lvalue.identifier.id, name.value);
71 + }
72 + break;
73 + }
74 + case 'PropertyLoad': {
75 + const objectName = names.get(value.object.identifier.id);
76 + if (objectName != null) {
77 + names.set(
78 + lvalue.identifier.id,
79 + `${objectName}.${String(value.property)}`,
80 + );
81 + }
82 + break;
83 + }
84 + case 'FunctionExpression': {
85 + const inner = nameAnonymousFunctionsImpl(value.loweredFunc.func);
86 + const node: Node = {
87 + fn: value,
88 + generatedName: null,
89 + inner,
90 + };
91 + /**
92 + * Bubble-up all functions, even if they're named, so that we can
93 + * later generate names for any inner anonymous functions
94 + */
95 + nodes.push(node);
96 + if (value.name == null) {
97 + // but only generate names for anonymous functions
98 + functions.set(lvalue.identifier.id, node);
99 + }
100 + break;
101 + }
102 + case 'StoreContext':
103 + case 'StoreLocal': {
104 + const node = functions.get(value.value.identifier.id);
105 + const variableName = value.lvalue.place.identifier.name;
106 + if (
107 + node != null &&
108 + variableName != null &&
109 + variableName.kind === 'named'
110 + ) {
111 + node.generatedName = variableName.value;
112 + functions.delete(value.value.identifier.id);
113 + }
114 + break;
115 + }
116 + case 'CallExpression':
117 + case 'MethodCall': {
118 + const callee =
119 + value.kind === 'MethodCall' ? value.property : value.callee;
120 + const hookKind = getHookKind(fn.env, callee.identifier);
121 + let calleeName: string | null = null;
122 + if (hookKind != null && hookKind !== 'Custom') {
123 + calleeName = hookKind;
124 + } else {
125 + calleeName = names.get(callee.identifier.id) ?? '(anonymous)';
126 + }
127 + let fnArgCount = 0;
128 + for (const arg of value.args) {
129 + if (arg.kind === 'Identifier' && functions.has(arg.identifier.id)) {
130 + fnArgCount++;
131 + }
132 + }
133 + for (let i = 0; i < value.args.length; i++) {
134 + const arg = value.args[i]!;
135 + if (arg.kind === 'Spread') {
136 + continue;
137 + }
138 + const node = functions.get(arg.identifier.id);
139 + if (node != null) {
140 + const generatedName =
141 + fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
142 + node.generatedName = generatedName;
143 + functions.delete(arg.identifier.id);
144 + }
145 + }
146 + break;
147 + }
148 + case 'JsxExpression': {
149 + for (const attr of value.props) {
150 + if (attr.kind === 'JsxSpreadAttribute') {
151 + continue;
152 + }
153 + const node = functions.get(attr.place.identifier.id);
154 + if (node != null) {
155 + const elementName =
156 + value.tag.kind === 'BuiltinTag'
157 + ? value.tag.name
158 + : (names.get(value.tag.identifier.id) ?? null);
159 + const propName =
160 + elementName == null
161 + ? attr.name
162 + : `<${elementName}>.${attr.name}`;
163 + node.generatedName = `${propName}`;
164 + functions.delete(attr.place.identifier.id);
165 + }
166 + }
167 + break;
168 + }
169 + }
170 + }
171 + }
172 + return nodes;
173 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.expect.md new
+272
@@ -0,0 +1,272 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNameAnonymousFunctions
6 +
7 +import {useEffect} from 'react';
8 +import {identity, Stringify, useIdentity} from 'shared-runtime';
9 +import * as SharedRuntime from 'shared-runtime';
10 +
11 +function Component(props) {
12 + function named() {
13 + const inner = () => props.named;
14 + return inner();
15 + }
16 + const namedVariable = function () {
17 + return props.namedVariable;
18 + };
19 + const methodCall = SharedRuntime.identity(() => props.methodCall);
20 + const call = identity(() => props.call);
21 + const builtinElementAttr = <div onClick={() => props.builtinElementAttr} />;
22 + const namedElementAttr = <Stringify onClick={() => props.namedElementAttr} />;
23 + const hookArgument = useIdentity(() => props.hookArgument);
24 + useEffect(() => {
25 + console.log(props.useEffect);
26 + JSON.stringify(null, null, () => props.useEffect);
27 + const g = () => props.useEffect;
28 + console.log(g());
29 + }, [props.useEffect]);
30 + return (
31 + <>
32 + {named()}
33 + {namedVariable()}
34 + {methodCall()}
35 + {call()}
36 + {builtinElementAttr}
37 + {namedElementAttr}
38 + {hookArgument()}
39 + </>
40 + );
41 +}
42 +
43 +export const TODO_FIXTURE_ENTRYPOINT = {
44 + fn: Component,
45 + params: [
46 + {
47 + named: '<named>',
48 + namedVariable: '<namedVariable>',
49 + methodCall: '<methodCall>',
50 + call: '<call>',
51 + builtinElementAttr: '<builtinElementAttr>',
52 + namedElementAttr: '<namedElementAttr>',
53 + hookArgument: '<hookArgument>',
54 + useEffect: '<useEffect>',
55 + },
56 + ],
57 +};
58 +
59 +```
60 +
61 +## Code
62 +
63 +```javascript
64 +import { c as _c } from "react/compiler-runtime"; // @enableNameAnonymousFunctions
65 +
66 +import { useEffect } from "react";
67 +import { identity, Stringify, useIdentity } from "shared-runtime";
68 +import * as SharedRuntime from "shared-runtime";
69 +
70 +function Component(props) {
71 + const $ = _c(31);
72 + let t0;
73 + if ($[0] !== props.named) {
74 + t0 = function named() {
75 + const inner = { "Component[named > inner]": () => props.named }[
76 + "Component[named > inner]"
77 + ];
78 + return inner();
79 + };
80 + $[0] = props.named;
81 + $[1] = t0;
82 + } else {
83 + t0 = $[1];
84 + }
85 + const named = t0;
86 + let t1;
87 + if ($[2] !== props.namedVariable) {
88 + t1 = {
89 + "Component[namedVariable]": function () {
90 + return props.namedVariable;
91 + },
92 + }["Component[namedVariable]"];
93 + $[2] = props.namedVariable;
94 + $[3] = t1;
95 + } else {
96 + t1 = $[3];
97 + }
98 + const namedVariable = t1;
99 + let t2;
100 + if ($[4] !== props.methodCall) {
101 + t2 = { "Component[SharedRuntime.identity()]": () => props.methodCall }[
102 + "Component[SharedRuntime.identity()]"
103 + ];
104 + $[4] = props.methodCall;
105 + $[5] = t2;
106 + } else {
107 + t2 = $[5];
108 + }
109 + const methodCall = SharedRuntime.identity(t2);
110 + let t3;
111 + if ($[6] !== props.call) {
112 + t3 = { "Component[identity()]": () => props.call }["Component[identity()]"];
113 + $[6] = props.call;
114 + $[7] = t3;
115 + } else {
116 + t3 = $[7];
117 + }
118 + const call = identity(t3);
119 + let t4;
120 + if ($[8] !== props.builtinElementAttr) {
121 + t4 = (
122 + <div
123 + onClick={
124 + { "Component[<div>.onClick]": () => props.builtinElementAttr }[
125 + "Component[<div>.onClick]"
126 + ]
127 + }
128 + />
129 + );
130 + $[8] = props.builtinElementAttr;
131 + $[9] = t4;
132 + } else {
133 + t4 = $[9];
134 + }
135 + const builtinElementAttr = t4;
136 + let t5;
137 + if ($[10] !== props.namedElementAttr) {
138 + t5 = (
139 + <Stringify
140 + onClick={
141 + { "Component[<Stringify>.onClick]": () => props.namedElementAttr }[
142 + "Component[<Stringify>.onClick]"
143 + ]
144 + }
145 + />
146 + );
147 + $[10] = props.namedElementAttr;
148 + $[11] = t5;
149 + } else {
150 + t5 = $[11];
151 + }
152 + const namedElementAttr = t5;
153 + let t6;
154 + if ($[12] !== props.hookArgument) {
155 + t6 = { "Component[useIdentity()]": () => props.hookArgument }[
156 + "Component[useIdentity()]"
157 + ];
158 + $[12] = props.hookArgument;
159 + $[13] = t6;
160 + } else {
161 + t6 = $[13];
162 + }
163 + const hookArgument = useIdentity(t6);
164 + let t7;
165 + let t8;
166 + if ($[14] !== props.useEffect) {
167 + t7 = {
168 + "Component[useEffect()]": () => {
169 + console.log(props.useEffect);
170 + JSON.stringify(
171 + null,
172 + null,
173 + {
174 + "Component[useEffect() > JSON.stringify()]": () => props.useEffect,
175 + }["Component[useEffect() > JSON.stringify()]"],
176 + );
177 + const g = { "Component[useEffect() > g]": () => props.useEffect }[
178 + "Component[useEffect() > g]"
179 + ];
180 + console.log(g());
181 + },
182 + }["Component[useEffect()]"];
183 + t8 = [props.useEffect];
184 + $[14] = props.useEffect;
185 + $[15] = t7;
186 + $[16] = t8;
187 + } else {
188 + t7 = $[15];
189 + t8 = $[16];
190 + }
191 + useEffect(t7, t8);
192 + let t9;
193 + if ($[17] !== named) {
194 + t9 = named();
195 + $[17] = named;
196 + $[18] = t9;
197 + } else {
198 + t9 = $[18];
199 + }
200 + let t10;
201 + if ($[19] !== namedVariable) {
202 + t10 = namedVariable();
203 + $[19] = namedVariable;
204 + $[20] = t10;
205 + } else {
206 + t10 = $[20];
207 + }
208 + const t11 = methodCall();
209 + const t12 = call();
210 + let t13;
211 + if ($[21] !== hookArgument) {
212 + t13 = hookArgument();
213 + $[21] = hookArgument;
214 + $[22] = t13;
215 + } else {
216 + t13 = $[22];
217 + }
218 + let t14;
219 + if (
220 + $[23] !== builtinElementAttr ||
221 + $[24] !== namedElementAttr ||
222 + $[25] !== t10 ||
223 + $[26] !== t11 ||
224 + $[27] !== t12 ||
225 + $[28] !== t13 ||
226 + $[29] !== t9
227 + ) {
228 + t14 = (
229 + <>
230 + {t9}
231 + {t10}
232 + {t11}
233 + {t12}
234 + {builtinElementAttr}
235 + {namedElementAttr}
236 + {t13}
237 + </>
238 + );
239 + $[23] = builtinElementAttr;
240 + $[24] = namedElementAttr;
241 + $[25] = t10;
242 + $[26] = t11;
243 + $[27] = t12;
244 + $[28] = t13;
245 + $[29] = t9;
246 + $[30] = t14;
247 + } else {
248 + t14 = $[30];
249 + }
250 + return t14;
251 +}
252 +
253 +export const TODO_FIXTURE_ENTRYPOINT = {
254 + fn: Component,
255 + params: [
256 + {
257 + named: "<named>",
258 + namedVariable: "<namedVariable>",
259 + methodCall: "<methodCall>",
260 + call: "<call>",
261 + builtinElementAttr: "<builtinElementAttr>",
262 + namedElementAttr: "<namedElementAttr>",
263 + hookArgument: "<hookArgument>",
264 + useEffect: "<useEffect>",
265 + },
266 + ],
267 +};
268 +
269 +```
270 +
271 +### Eval output
272 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/name-anonymous-functions.js new
+53
@@ -0,0 +1,53 @@
1 +// @enableNameAnonymousFunctions
2 +
3 +import {useEffect} from 'react';
4 +import {identity, Stringify, useIdentity} from 'shared-runtime';
5 +import * as SharedRuntime from 'shared-runtime';
6 +
7 +function Component(props) {
8 + function named() {
9 + const inner = () => props.named;
10 + return inner();
11 + }
12 + const namedVariable = function () {
13 + return props.namedVariable;
14 + };
15 + const methodCall = SharedRuntime.identity(() => props.methodCall);
16 + const call = identity(() => props.call);
17 + const builtinElementAttr = <div onClick={() => props.builtinElementAttr} />;
18 + const namedElementAttr = <Stringify onClick={() => props.namedElementAttr} />;
19 + const hookArgument = useIdentity(() => props.hookArgument);
20 + useEffect(() => {
21 + console.log(props.useEffect);
22 + JSON.stringify(null, null, () => props.useEffect);
23 + const g = () => props.useEffect;
24 + console.log(g());
25 + }, [props.useEffect]);
26 + return (
27 + <>
28 + {named()}
29 + {namedVariable()}
30 + {methodCall()}
31 + {call()}
32 + {builtinElementAttr}
33 + {namedElementAttr}
34 + {hookArgument()}
35 + </>
36 + );
37 +}
38 +
39 +export const TODO_FIXTURE_ENTRYPOINT = {
40 + fn: Component,
41 + params: [
42 + {
43 + named: '<named>',
44 + namedVariable: '<namedVariable>',
45 + methodCall: '<methodCall>',
46 + call: '<call>',
47 + builtinElementAttr: '<builtinElementAttr>',
48 + namedElementAttr: '<namedElementAttr>',
49 + hookArgument: '<hookArgument>',
50 + useEffect: '<useEffect>',
51 + },
52 + ],
53 +};