[forgive] Add code action to remove dependency array (#33000)
Adds a new codeaction event in the compiler and handler in forgive. This allows you to remove a dependency array when you're editing a range that is within an autodep eligible function. 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/33000). * #33002 * #33001 * __->__ #33000 Co-authored-by: Jordan Brown <jmbrown@meta.com>
lauren committed
Apr 23, 2025 at 21:31 UTC
f765082996f056c2abb354eb43cec3a3bf535264
5 files changed
+125
-9
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+8
-2
@@ -183,7 +183,8 @@ export type LoggerEvent =
183
| CompileSkipEvent
184
| PipelineErrorEvent
185
| TimingEvent
186
- | AutoDepsDecorationsEvent;
186
+ | AutoDepsDecorationsEvent
187
+ | AutoDepsEligibleEvent;
188
189
export type CompileErrorEvent = {
190
kind: 'CompileError';
@@ -222,9 +223,14 @@ export type TimingEvent = {
223
};
224
export type AutoDepsDecorationsEvent = {
225
kind: 'AutoDepsDecorations';
225
- useEffectCallExpr: t.SourceLocation;
226
+ fnLoc: t.SourceLocation;
227
decorations: Array<t.SourceLocation>;
228
};
229
+export type AutoDepsEligibleEvent = {
230
+ kind: 'AutoDepsEligible';
231
+ fnLoc: t.SourceLocation;
232
+ depArrayLoc: t.SourceLocation;
233
+};
234
235
export type Logger = {
236
logEvent: (filename: string | null, event: LoggerEvent) => void;
compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
+25
-1
@@ -230,7 +230,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
230
if (typeof value.loc !== 'symbol') {
231
fn.env.logger?.logEvent(fn.env.filename, {
232
kind: 'AutoDepsDecorations',
233
- useEffectCallExpr: value.loc,
233
+ fnLoc: value.loc,
234
decorations,
235
});
236
}
@@ -258,6 +258,30 @@ export function inferEffectDependencies(fn: HIRFunction): void {
258
rewriteInstrs.set(instr.id, newInstructions);
259
fn.env.inferredEffectLocations.add(callee.loc);
260
}
261
+ } else if (
262
+ value.args.length >= 2 &&
263
+ value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) &&
264
+ value.args[0].kind === 'Identifier'
265
+ ) {
266
+ const penultimateArg = value.args[value.args.length - 2];
267
+ const depArrayArg = value.args[value.args.length - 1];
268
+ if (
269
+ depArrayArg.kind !== 'Spread' &&
270
+ penultimateArg.kind !== 'Spread' &&
271
+ typeof depArrayArg.loc !== 'symbol' &&
272
+ typeof penultimateArg.loc !== 'symbol' &&
273
+ typeof value.loc !== 'symbol'
274
+ ) {
275
+ fn.env.logger?.logEvent(fn.env.filename, {
276
+ kind: 'AutoDepsEligible',
277
+ fnLoc: value.loc,
278
+ depArrayLoc: {
279
+ ...depArrayArg.loc,
280
+ start: penultimateArg.loc.end,
281
+ end: depArrayArg.loc.end,
282
+ },
283
+ });
284
+ }
285
}
286
}
287
}
compiler/packages/react-forgive/server/src/index.ts
+75
-5
@@ -7,10 +7,13 @@
7
8
import {TextDocument} from 'vscode-languageserver-textdocument';
9
import {
10
+ CodeAction,
11
+ CodeActionKind,
12
CodeLens,
13
createConnection,
14
type InitializeParams,
15
type InitializeResult,
16
+ Position,
17
ProposedFeatures,
18
TextDocuments,
19
TextDocumentSyncKind,
@@ -29,7 +32,12 @@ import {
32
AutoDepsDecorationsRequest,
33
mapCompilerEventToLSPEvent,
34
} from './requests/autodepsdecorations';
32
-import {isPositionWithinRange} from './utils/range';
35
+import {
36
+ isPositionWithinRange,
37
+ isRangeWithinRange,
38
+ Range,
39
+ sourceLocationToRange,
40
+} from './utils/range';
41
42
const SUPPORTED_LANGUAGE_IDS = new Set([
43
'javascript',
@@ -44,6 +52,15 @@ const documents = new TextDocuments(TextDocument);
52
let compilerOptions: PluginOptions | null = null;
53
let compiledFns: Set<CompileSuccessEvent> = new Set();
54
let autoDepsDecorations: Array<AutoDepsDecorationsLSPEvent> = [];
55
+let codeActionEvents: Array<CodeActionLSPEvent> = [];
56
+
57
+type CodeActionLSPEvent = {
58
+ title: string;
59
+ kind: CodeActionKind;
60
+ newText: string;
61
+ anchorRange: Range;
62
+ editRange: {start: Position; end: Position};
63
+};
64
65
connection.onInitialize((_params: InitializeParams) => {
66
// TODO(@poteto) get config fr
@@ -85,6 +102,16 @@ connection.onInitialize((_params: InitializeParams) => {
102
if (event.kind === 'AutoDepsDecorations') {
103
autoDepsDecorations.push(mapCompilerEventToLSPEvent(event));
104
}
105
+ if (event.kind === 'AutoDepsEligible') {
106
+ const depArrayLoc = sourceLocationToRange(event.depArrayLoc);
107
+ codeActionEvents.push({
108
+ title: 'Use React Compiler inferred dependency array',
109
+ kind: CodeActionKind.QuickFix,
110
+ newText: '',
111
+ anchorRange: sourceLocationToRange(event.fnLoc),
112
+ editRange: {start: depArrayLoc[0], end: depArrayLoc[1]},
113
+ });
114
+ }
115
},
116
},
117
};
@@ -92,6 +119,7 @@ connection.onInitialize((_params: InitializeParams) => {
119
capabilities: {
120
textDocumentSync: TextDocumentSyncKind.Full,
121
codeLensProvider: {resolveProvider: true},
122
+ codeActionProvider: {resolveProvider: true},
123
},
124
};
125
return result;
@@ -103,8 +131,7 @@ connection.onInitialized(() => {
131
132
documents.onDidChangeContent(async event => {
133
connection.console.info(`Changed: ${event.document.uri}`);
106
- compiledFns.clear();
107
- autoDepsDecorations = [];
134
+ resetState();
135
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
136
const text = event.document.getText();
137
await compile({
@@ -116,8 +143,7 @@ documents.onDidChangeContent(async event => {
143
});
144
145
connection.onDidChangeWatchedFiles(change => {
119
- compiledFns.clear();
120
- autoDepsDecorations = [];
146
+ resetState();
147
connection.console.log(
148
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
149
);
@@ -157,6 +183,44 @@ connection.onCodeLensResolve(lens => {
183
return lens;
184
});
185
186
+connection.onCodeAction(params => {
187
+ connection.console.log('onCodeAction');
188
+ connection.console.log(JSON.stringify(params, null, 2));
189
+ const codeActions: Array<CodeAction> = [];
190
+ for (const codeActionEvent of codeActionEvents) {
191
+ if (
192
+ isRangeWithinRange(
193
+ [params.range.start, params.range.end],
194
+ codeActionEvent.anchorRange,
195
+ )
196
+ ) {
197
+ codeActions.push(
198
+ CodeAction.create(
199
+ codeActionEvent.title,
200
+ {
201
+ changes: {
202
+ [params.textDocument.uri]: [
203
+ {
204
+ newText: codeActionEvent.newText,
205
+ range: codeActionEvent.editRange,
206
+ },
207
+ ],
208
+ },
209
+ },
210
+ codeActionEvent.kind,
211
+ ),
212
+ );
213
+ }
214
+ }
215
+ return codeActions;
216
+});
217
+
218
+connection.onCodeActionResolve(codeAction => {
219
+ connection.console.log('onCodeActionResolve');
220
+ connection.console.log(JSON.stringify(codeAction, null, 2));
221
+ return codeAction;
222
+});
223
+
224
connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
225
const position = params.position;
226
connection.console.debug('Client hovering on: ' + JSON.stringify(position));
@@ -168,6 +232,12 @@ connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
232
return null;
233
});
234
235
+function resetState() {
236
+ compiledFns.clear();
237
+ autoDepsDecorations = [];
238
+ codeActionEvents = [];
239
+}
240
+
241
documents.listen(connection);
242
connection.listen();
243
connection.console.info(`React Analyzer running in node ${process.version}`);
compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts
+1
-1
@@ -22,7 +22,7 @@ export function mapCompilerEventToLSPEvent(
22
event: AutoDepsDecorationsEvent,
23
): AutoDepsDecorationsLSPEvent {
24
return {
25
- useEffectCallExpr: sourceLocationToRange(event.useEffectCallExpr),
25
+ useEffectCallExpr: sourceLocationToRange(event.fnLoc),
26
decorations: event.decorations.map(sourceLocationToRange),
27
};
28
}
compiler/packages/react-forgive/server/src/utils/range.ts
+16
@@ -2,6 +2,7 @@ import * as t from '@babel/types';
2
import {type Position} from 'vscode-languageserver/node';
3
4
export type Range = [Position, Position];
5
+
6
export function isPositionWithinRange(
7
position: Position,
8
[start, end]: Range,
@@ -9,6 +10,21 @@ export function isPositionWithinRange(
10
return position.line >= start.line && position.line <= end.line;
11
}
12
13
+export function isRangeWithinRange(aRange: Range, bRange: Range): boolean {
14
+ const startComparison = comparePositions(aRange[0], bRange[0]);
15
+ const endComparison = comparePositions(aRange[1], bRange[1]);
16
+ return startComparison >= 0 && endComparison <= 0;
17
+}
18
+
19
+function comparePositions(a: Position, b: Position): number {
20
+ const lineComparison = a.line - b.line;
21
+ if (lineComparison === 0) {
22
+ return a.character - b.character;
23
+ } else {
24
+ return lineComparison;
25
+ }
26
+}
27
+
28
export function sourceLocationToRange(
29
loc: t.SourceLocation,
30
): [Position, Position] {