diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index 5ede6fec5ce58..0adbf3077c285 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -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; }; +export type AutoDepsEligibleEvent = { + kind: 'AutoDepsEligible'; + fnLoc: t.SourceLocation; + depArrayLoc: t.SourceLocation; +}; export type Logger = { logEvent: (filename: string | null, event: LoggerEvent) => void; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts index 275a1a91b154c..472e4cfae3cdc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts @@ -230,7 +230,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, }); } @@ -258,6 +258,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, + }, + }); + } } } } diff --git a/compiler/packages/react-forgive/server/src/index.ts b/compiler/packages/react-forgive/server/src/index.ts index 6432e3efc6aea..e4b75df19442a 100644 --- a/compiler/packages/react-forgive/server/src/index.ts +++ b/compiler/packages/react-forgive/server/src/index.ts @@ -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 = new Set(); let autoDepsDecorations: Array = []; +let codeActionEvents: Array = []; + +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 = []; + 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}`); diff --git a/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts b/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts index 43f4ac1fb9a2b..6f3a4051fb941 100644 --- a/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts +++ b/compiler/packages/react-forgive/server/src/requests/autodepsdecorations.ts @@ -22,7 +22,7 @@ export function mapCompilerEventToLSPEvent( event: AutoDepsDecorationsEvent, ): AutoDepsDecorationsLSPEvent { return { - useEffectCallExpr: sourceLocationToRange(event.useEffectCallExpr), + useEffectCallExpr: sourceLocationToRange(event.fnLoc), decorations: event.decorations.map(sourceLocationToRange), }; } diff --git a/compiler/packages/react-forgive/server/src/utils/range.ts b/compiler/packages/react-forgive/server/src/utils/range.ts index e0665ba8fe8c2..8a16f1bc096a4 100644 --- a/compiler/packages/react-forgive/server/src/utils/range.ts +++ b/compiler/packages/react-forgive/server/src/utils/range.ts @@ -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] {