Skip to content

Commit 114c30c

Browse files
committed
[compiler] Option to infer names for anonymous functions
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()]`.
1 parent f5e96b9 commit 114c30c

File tree

8 files changed

+465
-2
lines changed

8 files changed

+465
-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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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: Map<IdentifierId, FunctionExpression> = new Map();
21+
const names: Map<IdentifierId, string> = new Map();
22+
for (const block of fn.body.blocks.values()) {
23+
for (const instr of block.instructions) {
24+
const {lvalue, value} = instr;
25+
switch (value.kind) {
26+
case 'LoadGlobal': {
27+
names.set(lvalue.identifier.id, value.binding.name);
28+
break;
29+
}
30+
case 'LoadContext':
31+
case 'LoadLocal': {
32+
const name = value.place.identifier.name;
33+
if (name != null && name.kind === 'named') {
34+
names.set(lvalue.identifier.id, name.value);
35+
}
36+
break;
37+
}
38+
case 'PropertyLoad': {
39+
const objectName = names.get(value.object.identifier.id);
40+
if (objectName != null) {
41+
names.set(
42+
lvalue.identifier.id,
43+
`${objectName}.${String(value.property)}`,
44+
);
45+
}
46+
break;
47+
}
48+
case 'FunctionExpression': {
49+
if (value.name == null) {
50+
// only track anonymous functions
51+
functions.set(lvalue.identifier.id, value);
52+
}
53+
break;
54+
}
55+
case 'StoreContext':
56+
case 'StoreLocal': {
57+
const fn = functions.get(value.value.identifier.id);
58+
const variableName = value.lvalue.place.identifier.name;
59+
if (
60+
fn != null &&
61+
variableName != null &&
62+
variableName.kind === 'named'
63+
) {
64+
fn.name = `${parentName}[${variableName.value}]`;
65+
functions.delete(value.value.identifier.id);
66+
}
67+
break;
68+
}
69+
case 'CallExpression':
70+
case 'MethodCall': {
71+
const callee =
72+
value.kind === 'MethodCall' ? value.property : value.callee;
73+
const hookKind = getHookKind(fn.env, callee.identifier);
74+
let calleeName: string | null = null;
75+
if (hookKind != null && hookKind !== 'Custom') {
76+
calleeName = hookKind;
77+
} else {
78+
calleeName = names.get(callee.identifier.id) ?? '(anonymous)';
79+
}
80+
for (const arg of value.args) {
81+
if (arg.kind === 'Spread') {
82+
continue;
83+
}
84+
const fn = functions.get(arg.identifier.id);
85+
if (fn != null) {
86+
fn.name = `${parentName}[${calleeName}()]`;
87+
functions.delete(arg.identifier.id);
88+
}
89+
}
90+
break;
91+
}
92+
case 'JsxExpression': {
93+
for (const attr of value.props) {
94+
if (attr.kind === 'JsxSpreadAttribute') {
95+
continue;
96+
}
97+
const fn = functions.get(attr.place.identifier.id);
98+
if (fn != null) {
99+
const elementName =
100+
value.tag.kind === 'BuiltinTag'
101+
? value.tag.name
102+
: (names.get(value.tag.identifier.id) ?? null);
103+
const propName =
104+
elementName == null
105+
? attr.name
106+
: `<${elementName}>.${attr.name}`;
107+
fn.name = `${parentName}[${propName}]`;
108+
functions.delete(attr.place.identifier.id);
109+
}
110+
}
111+
break;
112+
}
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)