[forgive] Refactor inferred deps (#32999)
Refactor. Co-authored-by: Jordan Brown <jmbrown@meta.com> --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32999). * #33002 * #33001 * #33000 * __->__ #32999 Co-authored-by: Jordan Brown <jmbrown@meta.com>
lauren committed
Apr 23, 2025 at 21:27 UTC
7b21c464890ec991d95c88b5d68802a6c8b3bd8e
10 files changed
+196
-66
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+2
-2
@@ -183,7 +183,7 @@ export type LoggerEvent =
183
| CompileSkipEvent
184
| PipelineErrorEvent
185
| TimingEvent
186
- | AutoDepsDecorations;
186
+ | AutoDepsDecorationsEvent;
187
188
export type CompileErrorEvent = {
189
kind: 'CompileError';
@@ -220,7 +220,7 @@ export type TimingEvent = {
220
kind: 'Timing';
221
measurement: PerformanceMeasure;
222
};
223
-export type AutoDepsDecorations = {
223
+export type AutoDepsDecorationsEvent = {
224
kind: 'AutoDepsDecorations';
225
useEffectCallExpr: t.SourceLocation;
226
decorations: Array<t.SourceLocation>;
compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
+4
-1
@@ -380,7 +380,10 @@ function collectDepUsages(
380
381
for (const [, block] of fnExpr.loweredFunc.func.body.blocks) {
382
for (const instr of block.instructions) {
383
- if (instr.value.kind === 'LoadLocal') {
383
+ if (
384
+ instr.value.kind === 'LoadLocal' &&
385
+ identifiers.has(instr.value.place.identifier.id)
386
+ ) {
387
loadedDeps.add(instr.lvalue.identifier.id);
388
}
389
for (const place of eachInstructionOperand(instr)) {
compiler/packages/react-forgive/client/src/colors.ts
new
+73
@@ -0,0 +1,73 @@
1
+type RGB = [number, number, number];
2
+
3
+const int = Math.floor;
4
+
5
+export class Color {
6
+ constructor(
7
+ private r: number,
8
+ private g: number,
9
+ private b: number,
10
+ ) {}
11
+
12
+ toAlphaString(a: number) {
13
+ return this.toCssString(a);
14
+ }
15
+ toString() {
16
+ return this.toCssString(1);
17
+ }
18
+
19
+ /**
20
+ * Adjust the color by a multiplier to lighten (`> 1.0`) or darken (`< 1.0`) the color. Returns a new
21
+ * instance.
22
+ */
23
+ adjusted(mult: number) {
24
+ const adjusted = Color.redistribute([
25
+ this.r * mult,
26
+ this.g * mult,
27
+ this.b * mult,
28
+ ]);
29
+ return new Color(...adjusted);
30
+ }
31
+
32
+ private toCssString(a: number) {
33
+ return `rgba(${this.r},${this.g},${this.b},${a})`;
34
+ }
35
+ /**
36
+ * Redistributes rgb, maintaing hue until its clamped.
37
+ * https://stackoverflow.com/a/141943
38
+ */
39
+ private static redistribute([r, g, b]: RGB): RGB {
40
+ const threshold = 255.999;
41
+ const max = Math.max(r, g, b);
42
+ if (max <= threshold) {
43
+ return [int(r), int(g), int(b)];
44
+ }
45
+ const total = r + g + b;
46
+ if (total >= 3 * threshold) {
47
+ return [int(threshold), int(threshold), int(threshold)];
48
+ }
49
+ const x = (3 * threshold - total) / (3 * max - total);
50
+ const gray = threshold - x * max;
51
+ return [int(gray + x * r), int(gray + x * g), int(gray + x * b)];
52
+ }
53
+}
54
+
55
+export const BLACK = new Color(0, 0, 0);
56
+export const WHITE = new Color(255, 255, 255);
57
+
58
+const COLOR_POOL = [
59
+ new Color(249, 65, 68),
60
+ new Color(243, 114, 44),
61
+ new Color(248, 150, 30),
62
+ new Color(249, 132, 74),
63
+ new Color(249, 199, 79),
64
+ new Color(144, 190, 109),
65
+ new Color(67, 170, 139),
66
+ new Color(77, 144, 142),
67
+ new Color(87, 117, 144),
68
+ new Color(39, 125, 161),
69
+];
70
+
71
+export function getColorFor(index: number): Color {
72
+ return COLOR_POOL[Math.abs(index) % COLOR_POOL.length]!;
73
+}
compiler/packages/react-forgive/client/src/extension.ts
+43
-17
@@ -4,12 +4,30 @@ import * as vscode from 'vscode';
4
import {
5
LanguageClient,
6
LanguageClientOptions,
7
- Position,
7
+ type Position,
8
+ RequestType,
9
ServerOptions,
10
TransportKind,
11
} from 'vscode-languageclient/node';
12
+import {WHITE} from './colors';
13
14
let client: LanguageClient;
15
+const inferredEffectDepDecoration =
16
+ vscode.window.createTextEditorDecorationType({
17
+ backgroundColor: WHITE.toAlphaString(0.3),
18
+ });
19
+
20
+type Range = [Position, Position];
21
+interface AutoDepsDecorationsParams {
22
+ position: Position;
23
+}
24
+namespace AutoDepsDecorationsRequest {
25
+ export const type = new RequestType<
26
+ AutoDepsDecorationsParams,
27
+ Array<Range> | null,
28
+ void
29
+ >('react/autodeps_decorations');
30
+}
31
32
export function activate(context: vscode.ExtensionContext) {
33
const serverModule = context.asAbsolutePath(path.join('dist', 'server.js'));
@@ -54,23 +72,24 @@ export function activate(context: vscode.ExtensionContext) {
72
vscode.languages.registerHoverProvider(documentSelector, {
73
provideHover(_document, position, _token) {
74
client
57
- .sendRequest('react/autodepsdecorations', position)
58
- .then((decorations: Array<[Position, Position]>) => {
59
- for (const [start, end] of decorations) {
60
- const range = new vscode.Range(
61
- new vscode.Position(start.line, start.character),
62
- new vscode.Position(end.line, end.character),
75
+ .sendRequest(AutoDepsDecorationsRequest.type, {position})
76
+ .then(decorations => {
77
+ if (Array.isArray(decorations)) {
78
+ const decorationOptions = decorations.map(([start, end]) => {
79
+ return {
80
+ range: new vscode.Range(
81
+ new vscode.Position(start.line, start.character),
82
+ new vscode.Position(end.line, end.character),
83
+ ),
84
+ hoverMessage: 'Inferred as an effect dependency',
85
+ };
86
+ });
87
+ vscode.window.activeTextEditor?.setDecorations(
88
+ inferredEffectDepDecoration,
89
+ decorationOptions,
90
);
64
- const vscodeDecoration =
65
- vscode.window.createTextEditorDecorationType({
66
- backgroundColor: 'red',
67
- });
68
- vscode.window.activeTextEditor?.setDecorations(vscodeDecoration, [
69
- {
70
- range,
71
- hoverMessage: 'hehe',
72
- },
73
- ]);
91
+ } else {
92
+ clearDecorations(inferredEffectDepDecoration);
93
}
94
});
95
return null;
@@ -85,4 +104,11 @@ export function deactivate(): Thenable<void> | undefined {
104
if (client !== undefined) {
105
return client.stop();
106
}
107
+ return;
108
+}
109
+
110
+export function clearDecorations(
111
+ decorationType: vscode.TextEditorDecorationType,
112
+) {
113
+ vscode.window.activeTextEditor?.setDecorations(decorationType, []);
114
}
compiler/packages/react-forgive/server/src/custom-requests/autodepsdecorations.ts
deleted
-18
@@ -1,18 +0,0 @@
1
-import {AutoDepsDecorations} from 'babel-plugin-react-compiler/src/Entrypoint';
2
-import {Position} from 'vscode-languageserver-textdocument';
3
-import {sourceLocationToRange} from '../utils/lsp-adapter';
4
-
5
-export type Range = [Position, Position];
6
-export type AutoDepsDecorationsLSPEvent = {
7
- useEffectCallExpr: Range;
8
- decorations: Array<Range>;
9
-};
10
-
11
-export function mapCompilerEventToLSPEvent(
12
- event: AutoDepsDecorations,
13
-): AutoDepsDecorationsLSPEvent {
14
- return {
15
- useEffectCallExpr: sourceLocationToRange(event.useEffectCallExpr),
16
- decorations: event.decorations.map(sourceLocationToRange),
17
- };
18
-}
compiler/packages/react-forgive/server/src/index.ts
+11
-17
@@ -5,7 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {Position, TextDocument} from 'vscode-languageserver-textdocument';
8
+import {TextDocument} from 'vscode-languageserver-textdocument';
9
import {
10
CodeLens,
11
createConnection,
@@ -19,15 +19,17 @@ import {compile, lastResult} from './compiler';
19
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
20
import {resolveReactConfig} from './compiler/options';
21
import {
22
- CompileSuccessEvent,
22
+ type CompileSuccessEvent,
23
+ type LoggerEvent,
24
defaultOptions,
24
- LoggerEvent,
25
} from 'babel-plugin-react-compiler/src/Entrypoint/Options';
26
import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
27
import {
28
- AutoDepsDecorationsLSPEvent,
28
+ type AutoDepsDecorationsLSPEvent,
29
+ AutoDepsDecorationsRequest,
30
mapCompilerEventToLSPEvent,
30
-} from './custom-requests/autodepsdecorations';
31
+} from './requests/autodepsdecorations';
32
+import {isPositionWithinRange} from './utils/range';
33
34
const SUPPORTED_LANGUAGE_IDS = new Set([
35
'javascript',
@@ -155,19 +157,11 @@ connection.onCodeLensResolve(lens => {
157
return lens;
158
});
159
158
-connection.onRequest('react/autodepsdecorations', (position: Position) => {
159
- connection.console.log('Client hovering on: ' + JSON.stringify(position));
160
- connection.console.log(JSON.stringify(autoDepsDecorations, null, 2));
161
-
160
+connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
161
+ const position = params.position;
162
+ connection.console.debug('Client hovering on: ' + JSON.stringify(position));
163
for (const dec of autoDepsDecorations) {
163
- // TODO: extract to helper
164
- if (
165
- position.line >= dec.useEffectCallExpr[0].line &&
166
- position.line <= dec.useEffectCallExpr[1].line
167
- ) {
168
- connection.console.log(
169
- 'found decoration: ' + JSON.stringify(dec.decorations),
170
- );
164
+ if (isPositionWithinRange(position, dec.useEffectCallExpr)) {
165
return dec.decorations;
166
}
167
}
compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts
new
+28
@@ -0,0 +1,28 @@
1
+import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
2
+import {type Position} from 'vscode-languageserver-textdocument';
3
+import {RequestType} from 'vscode-languageserver/node';
4
+import {type Range, sourceLocationToRange} from '../utils/range';
5
+
6
+export type AutoDepsDecorationsLSPEvent = {
7
+ useEffectCallExpr: Range;
8
+ decorations: Array<Range>;
9
+};
10
+export interface AutoDepsDecorationsParams {
11
+ position: Position;
12
+}
13
+export namespace AutoDepsDecorationsRequest {
14
+ export const type = new RequestType<
15
+ AutoDepsDecorationsParams,
16
+ Array<Range> | null,
17
+ void
18
+ >('react/autodeps_decorations');
19
+}
20
+
21
+export function mapCompilerEventToLSPEvent(
22
+ event: AutoDepsDecorationsEvent,
23
+): AutoDepsDecorationsLSPEvent {
24
+ return {
25
+ useEffectCallExpr: sourceLocationToRange(event.useEffectCallExpr),
26
+ decorations: event.decorations.map(sourceLocationToRange),
27
+ };
28
+}
compiler/packages/react-forgive/server/src/utils/lsp-adapter.ts
deleted
-11
@@ -1,11 +0,0 @@
1
-import * as t from '@babel/types';
2
-import {Position} from 'vscode-languageserver/node';
3
-
4
-export function sourceLocationToRange(
5
- loc: t.SourceLocation,
6
-): [Position, Position] {
7
- return [
8
- {line: loc.start.line - 1, character: loc.start.column},
9
- {line: loc.end.line - 1, character: loc.end.column},
10
- ];
11
-}
compiler/packages/react-forgive/server/src/utils/range.ts
new
+19
@@ -0,0 +1,19 @@
1
+import * as t from '@babel/types';
2
+import {type Position} from 'vscode-languageserver/node';
3
+
4
+export type Range = [Position, Position];
5
+export function isPositionWithinRange(
6
+ position: Position,
7
+ [start, end]: Range,
8
+): boolean {
9
+ return position.line >= start.line && position.line <= end.line;
10
+}
11
+
12
+export function sourceLocationToRange(
13
+ loc: t.SourceLocation,
14
+): [Position, Position] {
15
+ return [
16
+ {line: loc.start.line - 1, character: loc.start.column},
17
+ {line: loc.end.line - 1, character: loc.end.column},
18
+ ];
19
+}
compiler/packages/react-forgive/tsconfig.json
new
+16
@@ -0,0 +1,16 @@
1
+{
2
+ "extends": "@tsconfig/strictest/tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "rootDir": "../",
7
+ "noEmit": true,
8
+ "jsx": "react-jsxdev",
9
+ "lib": ["ES2022"],
10
+
11
+ "target": "ES2022",
12
+ "importsNotUsedAsValues": "remove",
13
+ },
14
+ "exclude": ["node_modules"],
15
+ "include": ["server/src/**/*.ts", "client/src/**/*.ts"],
16
+}