[forgive] Add code action to remove dependency array

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>
This commit is contained in:
Lauren Tan
2025-04-23 18:27:05 -04:00
co-authored by Jordan Brown
parent d6a957940c
commit 8f91c97d1c
5 changed files with 125 additions and 9 deletions
@@ -183,7 +183,8 @@ export type LoggerEvent =
| CompileSkipEvent
| PipelineErrorEvent
| TimingEvent
| AutoDepsDecorationsEvent;
| AutoDepsDecorationsEvent
| AutoDepsEligibleEvent;
export type CompileErrorEvent = {
kind: 'CompileError';
@@ -222,9 +223,14 @@ export type TimingEvent = {
};
export type AutoDepsDecorationsEvent = {
kind: 'AutoDepsDecorations';
useEffectCallExpr: t.SourceLocation;
fnLoc: t.SourceLocation;
decorations: Array<t.SourceLocation>;
};
export type AutoDepsEligibleEvent = {
kind: 'AutoDepsEligible';
fnLoc: t.SourceLocation;
depArrayLoc: t.SourceLocation;
};
export type Logger = {
logEvent: (filename: string | null, event: LoggerEvent) => void;
@@ -216,7 +216,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
if (typeof value.loc !== 'symbol') {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'AutoDepsDecorations',
useEffectCallExpr: value.loc,
fnLoc: value.loc,
decorations: collectDepUsages(usedDeps, fnExpr.value)
.map(loc => (typeof loc !== 'symbol' ? loc : null))
.filter(loc => loc !== null),
@@ -246,6 +246,30 @@ export function inferEffectDependencies(fn: HIRFunction): void {
rewriteInstrs.set(instr.id, newInstructions);
fn.env.inferredEffectLocations.add(callee.loc);
}
} else if (
value.args.length >= 2 &&
value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) &&
value.args[0].kind === 'Identifier'
) {
const penultimateArg = value.args[value.args.length - 2];
const depArrayArg = value.args[value.args.length - 1];
if (
depArrayArg.kind !== 'Spread' &&
penultimateArg.kind !== 'Spread' &&
typeof depArrayArg.loc !== 'symbol' &&
typeof penultimateArg.loc !== 'symbol' &&
typeof value.loc !== 'symbol'
) {
fn.env.logger?.logEvent(fn.env.filename, {
kind: 'AutoDepsEligible',
fnLoc: value.loc,
depArrayLoc: {
...depArrayArg.loc,
start: penultimateArg.loc.end,
end: depArrayArg.loc.end,
},
});
}
}
}
}
@@ -7,10 +7,13 @@
import {TextDocument} from 'vscode-languageserver-textdocument';
import {
CodeAction,
CodeActionKind,
CodeLens,
createConnection,
type InitializeParams,
type InitializeResult,
Position,
ProposedFeatures,
TextDocuments,
TextDocumentSyncKind,
@@ -29,7 +32,12 @@ import {
AutoDepsDecorationsRequest,
mapCompilerEventToLSPEvent,
} from './requests/autodepsdecorations';
import {isPositionWithinRange} from './utils/range';
import {
isPositionWithinRange,
isRangeWithinRange,
Range,
sourceLocationToRange,
} from './utils/range';
const SUPPORTED_LANGUAGE_IDS = new Set([
'javascript',
@@ -44,6 +52,15 @@ const documents = new TextDocuments(TextDocument);
let compilerOptions: PluginOptions | null = null;
let compiledFns: Set<CompileSuccessEvent> = new Set();
let autoDepsDecorations: Array<AutoDepsDecorationsLSPEvent> = [];
let codeActionEvents: Array<CodeActionLSPEvent> = [];
type CodeActionLSPEvent = {
title: string;
kind: CodeActionKind;
newText: string;
anchorRange: Range;
editRange: {start: Position; end: Position};
};
connection.onInitialize((_params: InitializeParams) => {
// TODO(@poteto) get config fr
@@ -85,6 +102,16 @@ connection.onInitialize((_params: InitializeParams) => {
if (event.kind === 'AutoDepsDecorations') {
autoDepsDecorations.push(mapCompilerEventToLSPEvent(event));
}
if (event.kind === 'AutoDepsEligible') {
const depArrayLoc = sourceLocationToRange(event.depArrayLoc);
codeActionEvents.push({
title: 'Use React Compiler inferred dependency array',
kind: CodeActionKind.QuickFix,
newText: '',
anchorRange: sourceLocationToRange(event.fnLoc),
editRange: {start: depArrayLoc[0], end: depArrayLoc[1]},
});
}
},
},
};
@@ -92,6 +119,7 @@ connection.onInitialize((_params: InitializeParams) => {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full,
codeLensProvider: {resolveProvider: true},
codeActionProvider: {resolveProvider: true},
},
};
return result;
@@ -103,8 +131,7 @@ connection.onInitialized(() => {
documents.onDidChangeContent(async event => {
connection.console.info(`Changed: ${event.document.uri}`);
compiledFns.clear();
autoDepsDecorations = [];
resetState();
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
const text = event.document.getText();
await compile({
@@ -116,8 +143,7 @@ documents.onDidChangeContent(async event => {
});
connection.onDidChangeWatchedFiles(change => {
compiledFns.clear();
autoDepsDecorations = [];
resetState();
connection.console.log(
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
);
@@ -157,6 +183,44 @@ connection.onCodeLensResolve(lens => {
return lens;
});
connection.onCodeAction(params => {
connection.console.log('onCodeAction');
connection.console.log(JSON.stringify(params, null, 2));
const codeActions: Array<CodeAction> = [];
for (const codeActionEvent of codeActionEvents) {
if (
isRangeWithinRange(
[params.range.start, params.range.end],
codeActionEvent.anchorRange,
)
) {
codeActions.push(
CodeAction.create(
codeActionEvent.title,
{
changes: {
[params.textDocument.uri]: [
{
newText: codeActionEvent.newText,
range: codeActionEvent.editRange,
},
],
},
},
codeActionEvent.kind,
),
);
}
}
return codeActions;
});
connection.onCodeActionResolve(codeAction => {
connection.console.log('onCodeActionResolve');
connection.console.log(JSON.stringify(codeAction, null, 2));
return codeAction;
});
connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
const position = params.position;
connection.console.debug('Client hovering on: ' + JSON.stringify(position));
@@ -168,6 +232,12 @@ connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
return null;
});
function resetState() {
compiledFns.clear();
autoDepsDecorations = [];
codeActionEvents = [];
}
documents.listen(connection);
connection.listen();
connection.console.info(`React Analyzer running in node ${process.version}`);
@@ -22,7 +22,7 @@ export function mapCompilerEventToLSPEvent(
event: AutoDepsDecorationsEvent,
): AutoDepsDecorationsLSPEvent {
return {
useEffectCallExpr: sourceLocationToRange(event.useEffectCallExpr),
useEffectCallExpr: sourceLocationToRange(event.fnLoc),
decorations: event.decorations.map(sourceLocationToRange),
};
}
@@ -2,6 +2,7 @@ import * as t from '@babel/types';
import {type Position} from 'vscode-languageserver/node';
export type Range = [Position, Position];
export function isPositionWithinRange(
position: Position,
[start, end]: Range,
@@ -9,6 +10,21 @@ export function isPositionWithinRange(
return position.line >= start.line && position.line <= end.line;
}
export function isRangeWithinRange(aRange: Range, bRange: Range): boolean {
const startComparison = comparePositions(aRange[0], bRange[0]);
const endComparison = comparePositions(aRange[1], bRange[1]);
return startComparison >= 0 && endComparison <= 0;
}
function comparePositions(a: Position, b: Position): number {
const lineComparison = a.line - b.line;
if (lineComparison === 0) {
return a.character - b.character;
} else {
return lineComparison;
}
}
export function sourceLocationToRange(
loc: t.SourceLocation,
): [Position, Position] {