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

[forgive] Hacky first pass at adding decorations for inferred deps (#32998)

Draws basic decorations for inferred deps on hover. 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/32998). * #33002 * #33001 * #33000 * #32999 * __->__ #32998 Co-authored-by: Jordan Brown <jmbrown@meta.com>

lauren committed Apr 23, 2025 at 21:21 UTC e25e8c7575350a1dd217f56e7dcf530f14a90080
6 files changed +145 -18
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+2 -2
@@ -222,8 +222,8 @@ export type TimingEvent = {
222 };
223 export type AutoDepsDecorations = {
224 kind: 'AutoDepsDecorations';
225 - useEffectCallExpr: t.SourceLocation | null;
226 - decorations: Array<t.SourceLocation | null>;
225 + useEffectCallExpr: t.SourceLocation;
226 + decorations: Array<t.SourceLocation>;
227 };
228
229 export type Logger = {
compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
+22 -8
@@ -1,3 +1,11 @@
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 * as t from '@babel/types';
9 import {CompilerError, SourceLocation} from '..';
10 import {
11 ArrayExpression,
@@ -212,14 +220,20 @@ export function inferEffectDependencies(fn: HIRFunction): void {
220 }
221
222 // For LSP autodeps feature.
215 - fn.env.logger?.logEvent(fn.env.filename, {
216 - kind: 'AutoDepsDecorations',
217 - useEffectCallExpr:
218 - typeof value.loc !== 'symbol' ? value.loc : null,
219 - decorations: collectDepUsages(usedDeps, fnExpr.value).map(loc =>
220 - typeof loc !== 'symbol' ? loc : null,
221 - ),
222 - });
223 + const decorations: Array<t.SourceLocation> = [];
224 + for (const loc of collectDepUsages(usedDeps, fnExpr.value)) {
225 + if (typeof loc === 'symbol') {
226 + continue;
227 + }
228 + decorations.push(loc);
229 + }
230 + if (typeof value.loc !== 'symbol') {
231 + fn.env.logger?.logEvent(fn.env.filename, {
232 + kind: 'AutoDepsDecorations',
233 + useEffectCallExpr: value.loc,
234 + decorations,
235 + });
236 + }
237
238 newInstructions.push({
239 id: makeInstructionId(0),
compiler/packages/react-forgive/client/src/extension.ts
+35 -7
@@ -1,17 +1,22 @@
1 import * as path from 'path';
2 -import {ExtensionContext, window as Window} from 'vscode';
2 +import * as vscode from 'vscode';
3
4 import {
5 LanguageClient,
6 LanguageClientOptions,
7 + Position,
8 ServerOptions,
9 TransportKind,
10 } from 'vscode-languageclient/node';
11
12 let client: LanguageClient;
13
13 -export function activate(context: ExtensionContext) {
14 +export function activate(context: vscode.ExtensionContext) {
15 const serverModule = context.asAbsolutePath(path.join('dist', 'server.js'));
16 + const documentSelector = [
17 + {scheme: 'file', language: 'javascriptreact'},
18 + {scheme: 'file', language: 'typescriptreact'},
19 + ];
20
21 // If the extension is launched in debug mode then the debug server options are used
22 // Otherwise the run options are used
@@ -27,10 +32,7 @@ export function activate(context: ExtensionContext) {
32 };
33
34 const clientOptions: LanguageClientOptions = {
30 - documentSelector: [
31 - {scheme: 'file', language: 'javascriptreact'},
32 - {scheme: 'file', language: 'typescriptreact'},
33 - ],
35 + documentSelector,
36 progressOnInitialization: true,
37 };
38
@@ -43,12 +45,38 @@ export function activate(context: ExtensionContext) {
45 clientOptions,
46 );
47 } catch {
46 - Window.showErrorMessage(
48 + vscode.window.showErrorMessage(
49 `React Analyzer couldn't be started. See the output channel for details.`,
50 );
51 return;
52 }
53
54 + vscode.languages.registerHoverProvider(documentSelector, {
55 + provideHover(_document, position, _token) {
56 + 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),
63 + );
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 + ]);
74 + }
75 + });
76 + return null;
77 + },
78 + });
79 +
80 client.registerProposedFeatures();
81 client.start();
82 }
compiler/packages/react-forgive/server/src/custom-requests/autodepsdecorations.ts new
+18
@@ -0,0 +1,18 @@
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
+57 -1
@@ -5,7 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {TextDocument} from 'vscode-languageserver-textdocument';
8 +import {Position, TextDocument} from 'vscode-languageserver-textdocument';
9 import {
10 CodeLens,
11 createConnection,
@@ -24,6 +24,10 @@ import {
24 LoggerEvent,
25 } from 'babel-plugin-react-compiler/src/Entrypoint/Options';
26 import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
27 +import {
28 + AutoDepsDecorationsLSPEvent,
29 + mapCompilerEventToLSPEvent,
30 +} from './custom-requests/autodepsdecorations';
31
32 const SUPPORTED_LANGUAGE_IDS = new Set([
33 'javascript',
@@ -37,17 +41,48 @@ const documents = new TextDocuments(TextDocument);
41
42 let compilerOptions: PluginOptions | null = null;
43 let compiledFns: Set<CompileSuccessEvent> = new Set();
44 +let autoDepsDecorations: Array<AutoDepsDecorationsLSPEvent> = [];
45
46 connection.onInitialize((_params: InitializeParams) => {
47 // TODO(@poteto) get config fr
48 compilerOptions = resolveReactConfig('.') ?? defaultOptions;
49 compilerOptions = {
50 ...compilerOptions,
51 + environment: {
52 + ...compilerOptions.environment,
53 + inferEffectDependencies: [
54 + {
55 + function: {
56 + importSpecifierName: 'useEffect',
57 + source: 'react',
58 + },
59 + numRequiredArgs: 1,
60 + },
61 + {
62 + function: {
63 + importSpecifierName: 'useSpecialEffect',
64 + source: 'shared-runtime',
65 + },
66 + numRequiredArgs: 2,
67 + },
68 + {
69 + function: {
70 + importSpecifierName: 'default',
71 + source: 'useEffectWrapper',
72 + },
73 + numRequiredArgs: 1,
74 + },
75 + ],
76 + },
77 logger: {
78 logEvent(_filename: string | null, event: LoggerEvent) {
79 + connection.console.info(`Received event: ${event.kind}`);
80 if (event.kind === 'CompileSuccess') {
81 compiledFns.add(event);
82 }
83 + if (event.kind === 'AutoDepsDecorations') {
84 + autoDepsDecorations.push(mapCompilerEventToLSPEvent(event));
85 + }
86 },
87 },
88 };
@@ -67,6 +102,7 @@ connection.onInitialized(() => {
102 documents.onDidChangeContent(async event => {
103 connection.console.info(`Changed: ${event.document.uri}`);
104 compiledFns.clear();
105 + autoDepsDecorations = [];
106 if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
107 const text = event.document.getText();
108 await compile({
@@ -79,6 +115,7 @@ documents.onDidChangeContent(async event => {
115
116 connection.onDidChangeWatchedFiles(change => {
117 compiledFns.clear();
118 + autoDepsDecorations = [];
119 connection.console.log(
120 change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
121 );
@@ -118,6 +155,25 @@ connection.onCodeLensResolve(lens => {
155 return lens;
156 });
157
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 +
162 + 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 + );
171 + return dec.decorations;
172 + }
173 + }
174 + return null;
175 +});
176 +
177 documents.listen(connection);
178 connection.listen();
179 connection.console.info(`React Analyzer running in node ${process.version}`);
compiler/packages/react-forgive/server/src/utils/lsp-adapter.ts new
+11
@@ -0,0 +1,11 @@
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 +}