[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
+}