Skip to content

Commit a9410fb

Browse files
authored
[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
1 parent 6b70072 commit a9410fb

File tree

8 files changed

+544
-2
lines changed

8 files changed

+544
-2
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
103103
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
104104
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
105105
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
106+
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
106107

107108
export type CompilerPipelineValue =
108109
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -414,6 +415,15 @@ function runWithEnvironment(
414415
});
415416
}
416417

418+
if (env.config.enableNameAnonymousFunctions) {
419+
nameAnonymousFunctions(hir);
420+
log({
421+
kind: 'hir',
422+
name: 'NameAnonymougFunctions',
423+
value: hir,
424+
});
425+
}
426+
417427
const reactiveFunction = buildReactiveFunction(hir);
418428
log({
419429
kind: 'reactive',

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3566,6 +3566,8 @@ function lowerFunctionToValue(
35663566
let name: string | null = null;
35673567
if (expr.isFunctionExpression()) {
35683568
name = expr.get('id')?.node?.name ?? null;
3569+
} else if (expr.isFunctionDeclaration()) {
3570+
name = expr.get('id')?.node?.name ?? null;
35693571
}
35703572
const loweredFunc = lowerFunction(builder, expr);
35713573
if (!loweredFunc) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ export const EnvironmentConfigSchema = z.object({
261261

262262
enableFire: z.boolean().default(false),
263263

264+
enableNameAnonymousFunctions: z.boolean().default(false),
265+
264266
/**
265267
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
266268
* configurable module and import pairs to allow for user-land experimentation. For example,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {Type, makeType} from './Types';
1515
import {z} from 'zod';
1616
import type {AliasingEffect} from '../Inference/AliasingEffects';
1717
import {isReservedWord} from '../Utils/Keyword';
18+
import {Err, Ok, Result} from '../Utils/Result';
1819

1920
/*
2021
* *******************************************************************************************
@@ -1298,6 +1299,15 @@ export function forkTemporaryIdentifier(
12981299
};
12991300
}
13001301

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+
13011311
/**
13021312
* Creates a valid identifier name. This should *not* be used for synthesizing
13031313
* identifier names: only call this method for identifier names that appear in the

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
ValidIdentifierName,
4444
getHookKind,
4545
makeIdentifierName,
46+
validateIdentifierName,
4647
} from '../HIR/HIR';
4748
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
4849
import {eachPatternOperand} from '../HIR/visitors';
@@ -2326,6 +2327,11 @@ function codegenInstructionValue(
23262327
),
23272328
reactiveFunction,
23282329
).unwrap();
2330+
2331+
const validatedName =
2332+
instrValue.name != null
2333+
? validateIdentifierName(instrValue.name)
2334+
: Err(null);
23292335
if (instrValue.type === 'ArrowFunctionExpression') {
23302336
let body: t.BlockStatement | t.Expression = fn.body;
23312337
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -2337,14 +2343,28 @@ function codegenInstructionValue(
23372343
value = t.arrowFunctionExpression(fn.params, body, fn.async);
23382344
} else {
23392345
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),
23422349
fn.params,
23432350
fn.body,
23442351
fn.generator,
23452352
fn.async,
23462353
);
23472354
}
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+
}
23482368
break;
23492369
}
23502370
case 'TaggedTemplateExpression': {
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)