Repository: facebook/react
Pull request: [forgive] Emit AutoDepsDecoration event when inferring effect deps


Emits a new event for decorating inferred effect dependencies.

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/32997).
* #33002
* #33001
* #33000
* #32999
* #32998
* __->__ #32997
* #32996

Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
patch: @@ -182,7 +182,8 @@ export type LoggerEvent =
   | CompileDiagnosticEvent
   | CompileSkipEvent
   | PipelineErrorEvent
-  | TimingEvent;
+  | TimingEvent
+  | AutoDepsDecorations;
 
 export type CompileErrorEvent = {
   kind: 'CompileError';
@@ -219,6 +220,11 @@ export type TimingEvent = {
   kind: 'Timing';
   measurement: PerformanceMeasure;
 };
+export type AutoDepsDecorations = {
+  kind: 'AutoDepsDecorations';
+  useEffectCallExpr: t.SourceLocation | null;
+  decorations: Array<t.SourceLocation | null>;
+};
 
 export type Logger = {
   logEvent: (filename: string | null, event: LoggerEvent) => void;

filename: compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
patch: @@ -188,6 +188,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
              * the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
              * explanation.
              */
+            const usedDeps = [];
             for (const dep of scopeInfo.deps) {
               if (
                 ((isUseRefType(dep.identifier) ||
@@ -207,8 +208,19 @@ export function inferEffectDependencies(fn: HIRFunction): void {
               );
               newInstructions.push(...instructions);
               effectDeps.push(place);
+              usedDeps.push(dep);
             }
 
+            // For LSP autodeps feature.
+            fn.env.logger?.logEvent(fn.env.filename, {
+              kind: 'AutoDepsDecorations',
+              useEffectCallExpr:
+                typeof value.loc !== 'symbol' ? value.loc : null,
+              decorations: collectDepUsages(usedDeps, fnExpr.value).map(loc =>
+                typeof loc !== 'symbol' ? loc : null,
+              ),
+            });
+
             newInstructions.push({
               id: makeInstructionId(0),
               loc: GeneratedSource,
@@ -340,3 +352,31 @@ function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
   }
   return reactiveIds;
 }
+
+function collectDepUsages(
+  deps: Array<ReactiveScopeDependency>,
+  fnExpr: FunctionExpression,
+): Array<SourceLocation> {
+  const identifiers: Map<IdentifierId, ReactiveScopeDependency> = new Map();
+  const loadedDeps: Set<IdentifierId> = new Set();
+  const sourceLocations = [];
+  for (const dep of deps) {
+    identifiers.set(dep.identifier.id, dep);
+  }
+
+  for (const [, block] of fnExpr.loweredFunc.func.body.blocks) {
+    for (const instr of block.instructions) {
+      if (instr.value.kind === 'LoadLocal') {
+        loadedDeps.add(instr.lvalue.identifier.id);
+      }
+      for (const place of eachInstructionOperand(instr)) {
+        if (loadedDeps.has(place.identifier.id)) {
+          // TODO(@jbrown215): handle member exprs!!
+          sourceLocations.push(place.identifier.loc);
+        }
+      }
+    }
+  }
+
+  return sourceLocations;
+}