Skip to content

Commit 3e6538c

Browse files
committed
[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 DiffTrain build for [a9410fb](a9410fb)
1 parent b00e0c7 commit 3e6538c

35 files changed

+248
-89
lines changed

compiled/eslint-plugin-react-hooks/index.js

Lines changed: 162 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18614,6 +18614,12 @@ function makeTemporaryIdentifier(id, loc) {
1861418614
function forkTemporaryIdentifier(id, source) {
1861518615
return Object.assign(Object.assign({}, source), { mutableRange: { start: makeInstructionId(0), end: makeInstructionId(0) }, id });
1861618616
}
18617+
function validateIdentifierName(name) {
18618+
if (isReservedWord(name) || !libExports$1.isValidIdentifier(name)) {
18619+
return Err(null);
18620+
}
18621+
return Ok(makeIdentifierName(name).value);
18622+
}
1861718623
function makeIdentifierName(name) {
1861818624
if (isReservedWord(name)) {
1861918625
CompilerError.throwInvalidJS({
@@ -25842,13 +25848,16 @@ function trimJsxText(original) {
2584225848
}
2584325849
}
2584425850
function lowerFunctionToValue(builder, expr) {
25845-
var _a, _b, _c, _d;
25851+
var _a, _b, _c, _d, _e, _f, _g;
2584625852
const exprNode = expr.node;
2584725853
const exprLoc = (_a = exprNode.loc) !== null && _a !== void 0 ? _a : GeneratedSource;
2584825854
let name = null;
2584925855
if (expr.isFunctionExpression()) {
2585025856
name = (_d = (_c = (_b = expr.get('id')) === null || _b === void 0 ? void 0 : _b.node) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : null;
2585125857
}
25858+
else if (expr.isFunctionDeclaration()) {
25859+
name = (_g = (_f = (_e = expr.get('id')) === null || _e === void 0 ? void 0 : _e.node) === null || _f === void 0 ? void 0 : _f.name) !== null && _g !== void 0 ? _g : null;
25860+
}
2585225861
const loweredFunc = lowerFunction(builder, expr);
2585325862
if (!loweredFunc) {
2585425863
return { kind: 'UnsupportedNode', node: exprNode, loc: exprLoc };
@@ -32084,6 +32093,7 @@ const EnvironmentConfigSchema = zod.z.object({
3208432093
flowTypeProvider: zod.z.nullable(zod.z.function().args(zod.z.string())).default(null),
3208532094
enableOptionalDependencies: zod.z.boolean().default(true),
3208632095
enableFire: zod.z.boolean().default(false),
32096+
enableNameAnonymousFunctions: zod.z.boolean().default(false),
3208732097
inferEffectDependencies: zod.z
3208832098
.nullable(zod.z.array(zod.z.object({
3208932099
function: ExternalFunctionSchema,
@@ -38457,7 +38467,7 @@ function codegenInstructionValueToExpression(cx, instrValue) {
3845738467
return convertValueToExpression(value);
3845838468
}
3845938469
function codegenInstructionValue(cx, instrValue) {
38460-
var _a, _b, _c, _d, _e, _f, _g, _h;
38470+
var _a, _b, _c, _d, _e, _f, _g;
3846138471
let value;
3846238472
switch (instrValue.kind) {
3846338473
case 'ArrayExpression': {
@@ -38775,6 +38785,9 @@ function codegenInstructionValue(cx, instrValue) {
3877538785
pruneUnusedLValues(reactiveFunction);
3877638786
pruneHoistedContexts(reactiveFunction);
3877738787
const fn = codegenReactiveFunction(new Context$2(cx.env, (_g = reactiveFunction.id) !== null && _g !== void 0 ? _g : '[[ anonymous ]]', cx.uniqueIdentifiers, cx.fbtOperands, cx.temp), reactiveFunction).unwrap();
38788+
const validatedName = instrValue.name != null
38789+
? validateIdentifierName(instrValue.name)
38790+
: Err(null);
3877838791
if (instrValue.type === 'ArrowFunctionExpression') {
3877938792
let body = fn.body;
3878038793
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -38786,7 +38799,15 @@ function codegenInstructionValue(cx, instrValue) {
3878638799
value = libExports$1.arrowFunctionExpression(fn.params, body, fn.async);
3878738800
}
3878838801
else {
38789-
value = libExports$1.functionExpression((_h = fn.id) !== null && _h !== void 0 ? _h : (instrValue.name != null ? libExports$1.identifier(instrValue.name) : null), fn.params, fn.body, fn.generator, fn.async);
38802+
value = libExports$1.functionExpression(validatedName
38803+
.map(name => libExports$1.identifier(name))
38804+
.unwrapOr(null), fn.params, fn.body, fn.generator, fn.async);
38805+
}
38806+
if (cx.env.config.enableNameAnonymousFunctions &&
38807+
validatedName.isErr() &&
38808+
instrValue.name != null) {
38809+
const name = instrValue.name;
38810+
value = libExports$1.memberExpression(libExports$1.objectExpression([libExports$1.objectProperty(libExports$1.stringLiteral(name), value)]), libExports$1.stringLiteral(name), true, false);
3879038811
}
3879138812
break;
3879238813
}
@@ -51841,6 +51862,136 @@ function validateEffect(effectFunction, effectDeps, errors) {
5184151862
}
5184251863
}
5184351864

51865+
function nameAnonymousFunctions(fn) {
51866+
if (fn.id == null) {
51867+
return;
51868+
}
51869+
const parentName = fn.id;
51870+
const functions = nameAnonymousFunctionsImpl(fn);
51871+
function visit(node, prefix) {
51872+
var _a, _b;
51873+
if (node.generatedName != null) {
51874+
const name = `${prefix}${node.generatedName}]`;
51875+
node.fn.name = name;
51876+
}
51877+
const nextPrefix = `${prefix}${(_b = (_a = node.generatedName) !== null && _a !== void 0 ? _a : node.fn.name) !== null && _b !== void 0 ? _b : '<anonymous>'} > `;
51878+
for (const inner of node.inner) {
51879+
visit(inner, nextPrefix);
51880+
}
51881+
}
51882+
for (const node of functions) {
51883+
visit(node, `${parentName}[`);
51884+
}
51885+
}
51886+
function nameAnonymousFunctionsImpl(fn) {
51887+
var _a, _b;
51888+
const functions = new Map();
51889+
const names = new Map();
51890+
const nodes = [];
51891+
for (const block of fn.body.blocks.values()) {
51892+
for (const instr of block.instructions) {
51893+
const { lvalue, value } = instr;
51894+
switch (value.kind) {
51895+
case 'LoadGlobal': {
51896+
names.set(lvalue.identifier.id, value.binding.name);
51897+
break;
51898+
}
51899+
case 'LoadContext':
51900+
case 'LoadLocal': {
51901+
const name = value.place.identifier.name;
51902+
if (name != null && name.kind === 'named') {
51903+
names.set(lvalue.identifier.id, name.value);
51904+
}
51905+
break;
51906+
}
51907+
case 'PropertyLoad': {
51908+
const objectName = names.get(value.object.identifier.id);
51909+
if (objectName != null) {
51910+
names.set(lvalue.identifier.id, `${objectName}.${String(value.property)}`);
51911+
}
51912+
break;
51913+
}
51914+
case 'FunctionExpression': {
51915+
const inner = nameAnonymousFunctionsImpl(value.loweredFunc.func);
51916+
const node = {
51917+
fn: value,
51918+
generatedName: null,
51919+
inner,
51920+
};
51921+
nodes.push(node);
51922+
if (value.name == null) {
51923+
functions.set(lvalue.identifier.id, node);
51924+
}
51925+
break;
51926+
}
51927+
case 'StoreContext':
51928+
case 'StoreLocal': {
51929+
const node = functions.get(value.value.identifier.id);
51930+
const variableName = value.lvalue.place.identifier.name;
51931+
if (node != null &&
51932+
variableName != null &&
51933+
variableName.kind === 'named') {
51934+
node.generatedName = variableName.value;
51935+
functions.delete(value.value.identifier.id);
51936+
}
51937+
break;
51938+
}
51939+
case 'CallExpression':
51940+
case 'MethodCall': {
51941+
const callee = value.kind === 'MethodCall' ? value.property : value.callee;
51942+
const hookKind = getHookKind(fn.env, callee.identifier);
51943+
let calleeName = null;
51944+
if (hookKind != null && hookKind !== 'Custom') {
51945+
calleeName = hookKind;
51946+
}
51947+
else {
51948+
calleeName = (_a = names.get(callee.identifier.id)) !== null && _a !== void 0 ? _a : '(anonymous)';
51949+
}
51950+
let fnArgCount = 0;
51951+
for (const arg of value.args) {
51952+
if (arg.kind === 'Identifier' && functions.has(arg.identifier.id)) {
51953+
fnArgCount++;
51954+
}
51955+
}
51956+
for (let i = 0; i < value.args.length; i++) {
51957+
const arg = value.args[i];
51958+
if (arg.kind === 'Spread') {
51959+
continue;
51960+
}
51961+
const node = functions.get(arg.identifier.id);
51962+
if (node != null) {
51963+
const generatedName = fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
51964+
node.generatedName = generatedName;
51965+
functions.delete(arg.identifier.id);
51966+
}
51967+
}
51968+
break;
51969+
}
51970+
case 'JsxExpression': {
51971+
for (const attr of value.props) {
51972+
if (attr.kind === 'JsxSpreadAttribute') {
51973+
continue;
51974+
}
51975+
const node = functions.get(attr.place.identifier.id);
51976+
if (node != null) {
51977+
const elementName = value.tag.kind === 'BuiltinTag'
51978+
? value.tag.name
51979+
: ((_b = names.get(value.tag.identifier.id)) !== null && _b !== void 0 ? _b : null);
51980+
const propName = elementName == null
51981+
? attr.name
51982+
: `<${elementName}>.${attr.name}`;
51983+
node.generatedName = `${propName}`;
51984+
functions.delete(attr.place.identifier.id);
51985+
}
51986+
}
51987+
break;
51988+
}
51989+
}
51990+
}
51991+
}
51992+
return nodes;
51993+
}
51994+
5184451995
function run(func, config, fnType, mode, programContext, logger, filename, code) {
5184551996
var _a, _b;
5184651997
const contextIdentifiers = findContextIdentifiers(func);
@@ -52059,6 +52210,14 @@ function runWithEnvironment(func, env) {
5205952210
value: hir,
5206052211
});
5206152212
}
52213+
if (env.config.enableNameAnonymousFunctions) {
52214+
nameAnonymousFunctions(hir);
52215+
log({
52216+
kind: 'hir',
52217+
name: 'NameAnonymougFunctions',
52218+
value: hir,
52219+
});
52220+
}
5206252221
const reactiveFunction = buildReactiveFunction(hir);
5206352222
log({
5206452223
kind: 'reactive',

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3f2a42a5decc88551d34c96f3d031c316ac34f6a
1+
a9410fb487776339ec68e57a57a570be952ccad0
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3f2a42a5decc88551d34c96f3d031c316ac34f6a
1+
a9410fb487776339ec68e57a57a570be952ccad0

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,7 +1418,7 @@ __DEV__ &&
14181418
exports.useTransition = function () {
14191419
return resolveDispatcher().useTransition();
14201420
};
1421-
exports.version = "19.2.0-www-classic-3f2a42a5-20250908";
1421+
exports.version = "19.2.0-www-classic-a9410fb4-20250909";
14221422
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
14231423
"function" ===
14241424
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,7 +1418,7 @@ __DEV__ &&
14181418
exports.useTransition = function () {
14191419
return resolveDispatcher().useTransition();
14201420
};
1421-
exports.version = "19.2.0-www-modern-3f2a42a5-20250908";
1421+
exports.version = "19.2.0-www-modern-a9410fb4-20250909";
14221422
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
14231423
"function" ===
14241424
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,4 +600,4 @@ exports.useSyncExternalStore = function (
600600
exports.useTransition = function () {
601601
return ReactSharedInternals.H.useTransition();
602602
};
603-
exports.version = "19.2.0-www-classic-3f2a42a5-20250908";
603+
exports.version = "19.2.0-www-classic-a9410fb4-20250909";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,4 +600,4 @@ exports.useSyncExternalStore = function (
600600
exports.useTransition = function () {
601601
return ReactSharedInternals.H.useTransition();
602602
};
603-
exports.version = "19.2.0-www-modern-3f2a42a5-20250908";
603+
exports.version = "19.2.0-www-modern-a9410fb4-20250909";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ exports.useSyncExternalStore = function (
604604
exports.useTransition = function () {
605605
return ReactSharedInternals.H.useTransition();
606606
};
607-
exports.version = "19.2.0-www-classic-3f2a42a5-20250908";
607+
exports.version = "19.2.0-www-classic-a9410fb4-20250909";
608608
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
609609
"function" ===
610610
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ exports.useSyncExternalStore = function (
604604
exports.useTransition = function () {
605605
return ReactSharedInternals.H.useTransition();
606606
};
607-
exports.version = "19.2.0-www-modern-3f2a42a5-20250908";
607+
exports.version = "19.2.0-www-modern-a9410fb4-20250909";
608608
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
609609
"function" ===
610610
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19708,10 +19708,10 @@ __DEV__ &&
1970819708
(function () {
1970919709
var internals = {
1971019710
bundleType: 1,
19711-
version: "19.2.0-www-classic-3f2a42a5-20250908",
19711+
version: "19.2.0-www-classic-a9410fb4-20250909",
1971219712
rendererPackageName: "react-art",
1971319713
currentDispatcherRef: ReactSharedInternals,
19714-
reconcilerVersion: "19.2.0-www-classic-3f2a42a5-20250908"
19714+
reconcilerVersion: "19.2.0-www-classic-a9410fb4-20250909"
1971519715
};
1971619716
internals.overrideHookState = overrideHookState;
1971719717
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -19745,7 +19745,7 @@ __DEV__ &&
1974519745
exports.Shape = Shape;
1974619746
exports.Surface = Surface;
1974719747
exports.Text = Text;
19748-
exports.version = "19.2.0-www-classic-3f2a42a5-20250908";
19748+
exports.version = "19.2.0-www-classic-a9410fb4-20250909";
1974919749
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1975019750
"function" ===
1975119751
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

0 commit comments

Comments
 (0)