@samitouri / QOS-React-2 / commits / f765082996

[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] {