Skip to content

Commit

Permalink
[flow][codemod] Codemod to remove unnecessary React import
Browse files Browse the repository at this point in the history
Summary:
We detect it's useless by

- Running scope builder excluding types. If there is no other uses of React, then it's likely dead. However, we still need to check the following
- No mentions of React under typeof import, since these are annotations and skipped in the first step.
- No component syntax (technically only component with ref is problematic, but just to be consistent here. We should auto-import React with component syntax to remove this restriction in the future.)

Changelog: [feature] We added a new codemod `flow codemod remove-unnecessary-react-import` which can help remove unnecessary react imports under `react.runtime=automatic`

Reviewed By: jbrown215

Differential Revision: D54779424

fbshipit-source-id: 4d175576924887c7e4801ab99b5e648ef4a170ac
  • Loading branch information
SamChou19815 authored and facebook-github-bot committed Mar 12, 2024
1 parent 784c524 commit eff9470
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 0 deletions.
151 changes: 151 additions & 0 deletions src/codemods/remove_react_import.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
(*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)

module Ast = Flow_ast
module T = Ast.Type
module F = T.Function
module Id = Ast.Identifier
module TP = T.TypeParams
module GId = T.Generic.Identifier

module RemoveReactImportStats = struct
type t = {
removed: int;
still_useful: int;
}

let empty = { removed = 0; still_useful = 0 }

let combine x1 x2 =
{ removed = x1.removed + x2.removed; still_useful = x1.still_useful + x2.still_useful }

let add_removed x = { x with removed = x.removed + 1 }

let add_still_useful x = { x with still_useful = x.still_useful + 1 }

let serialize x =
[Utils_js.spf "removed: %d" x.removed; Utils_js.spf "still useful: %d" x.still_useful]

let report x =
[
Insert_type_utils.string_of_row ~indent:2 "Removed instances" x.removed;
Insert_type_utils.string_of_row ~indent:2 "Still useful instances" x.still_useful;
]
end

module Acc = Insert_type_utils.UntypedAcc (RemoveReactImportStats)

let react_import_def_loc_opt_of_stmt = function
| ( _,
Ast.Statement.ImportDeclaration
{
Ast.Statement.ImportDeclaration.import_kind = Ast.Statement.ImportDeclaration.ImportValue;
source = (_, { Ast.StringLiteral.value = "react"; _ });
default =
Some
{
Ast.Statement.ImportDeclaration.identifier =
(loc, { Ast.Identifier.name = "React"; _ });
_;
};
specifiers = None;
comments = _;
}
)
| ( _,
Ast.Statement.ImportDeclaration
{
Ast.Statement.ImportDeclaration.import_kind = Ast.Statement.ImportDeclaration.ImportValue;
source = (_, { Ast.StringLiteral.value = "react"; _ });
default = None;
specifiers =
Some
(Ast.Statement.ImportDeclaration.ImportNamespaceSpecifier
(_, (loc, { Ast.Identifier.name = "React"; _ }))
);
comments = _;
}
) ->
Some loc
| _ -> None

let has_unaccounted_react_value_usage_visitor =
object (this)
inherit [bool, Loc.t] Flow_ast_visitor.visitor ~init:false as super

val mutable in_typeof_type = false

method! typeof_type t =
let saved_in_typeof_type = in_typeof_type in
in_typeof_type <- true;
let t' = super#typeof_type t in
in_typeof_type <- saved_in_typeof_type;
t'

method! component_declaration loc c =
this#update_acc (fun _ -> true);
super#component_declaration loc c

method! identifier ((_, { Ast.Identifier.name; _ }) as id) =
if in_typeof_type && name = "React" then this#update_acc (fun _ -> true);
id
end

let mapper ctx =
object (this)
inherit [Acc.t] Codemod_ast_mapper.mapper "" ~init:Acc.empty

method! program ((prog_loc, ({ Ast.Program.statements; _ } as prog')) as prog) =
let file = ctx.Codemod_context.Untyped.file in
let react_import_def_loc =
Base.List.fold statements ~init:None ~f:(fun acc stmt ->
if Base.Option.is_some acc then
acc
else
react_import_def_loc_opt_of_stmt stmt
)
in
match react_import_def_loc with
| None -> prog
| Some react_import_def_loc ->
(* We intentionally exclude types so that the type uses of React is not counted. *)
let scope_info = Scope_builder.program ~enable_enums:false ~with_types:false prog in
let unused =
Scope_api.With_Loc.def_is_unused
scope_info
(Scope_api.With_Loc.def_of_use scope_info react_import_def_loc)
&& not
(has_unaccounted_react_value_usage_visitor#eval
has_unaccounted_react_value_usage_visitor#program
prog
)
in
this#update_acc (fun acc ->
let extra =
if unused then
RemoveReactImportStats.add_removed acc.Acc.stats
else
RemoveReactImportStats.add_still_useful acc.Acc.stats
in
Acc.update_stats acc extra
);
if unused then (
this#update_acc (fun acc ->
{ acc with Acc.changed_set = Utils_js.FilenameSet.add file acc.Acc.changed_set }
);
( prog_loc,
{
prog' with
Ast.Program.statements =
Base.List.filter statements ~f:(fun stmt ->
Base.Option.is_none (react_import_def_loc_opt_of_stmt stmt)
);
}
)
) else
prog
end
31 changes: 31 additions & 0 deletions src/commands/codemodCommand.ml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,36 @@ module KeyMirror_command = struct
let command = CommandSpec.command spec main
end

module RemoveReactImportCommand = struct
let doc = "Remove unnecessary imports of React under react.runtime=automatic."

let spec =
{
CommandSpec.name = "remove-unnecessary-react-import";
doc;
usage =
Printf.sprintf
"Usage: %s codemod remove-unnecessary-react-import [OPTION]... [FILE]\n\n%s\n"
Utils_js.exe_name
doc;
args = CommandSpec.ArgSpec.(empty |> CommandUtils.codemod_flags);
}

let main codemod_flags () =
let module Runner = Codemod_runner.MakeUntypedRunner (struct
module Acc = Remove_react_import.Acc

type accumulator = Acc.t

let reporter = string_reporter (module Acc)

let visit = Codemod_utils.make_visitor (Codemod_utils.Mapper Remove_react_import.mapper)
end) in
main (module Runner) codemod_flags ()

let command = CommandSpec.command spec main
end

module Annotate_optional_properties_command = struct
let doc = "Inserts optional properties on object definitions where properties are missing."

Expand Down Expand Up @@ -262,6 +292,7 @@ let command =
Annotate_optional_properties_command.command
);
(KeyMirror_command.spec.CommandSpec.name, KeyMirror_command.command);
(RemoveReactImportCommand.spec.CommandSpec.name, RemoveReactImportCommand.command);
]
in
CommandSpec.command spec main
4 changes: 4 additions & 0 deletions tests/codemod_remove_react_import/.flowconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[options]
all=true
react.runtime=automatic
component_syntax=true
1 change: 1 addition & 0 deletions tests/codemod_remove_react_import/.testconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cmd: codemod remove-unnecessary-react-import --strip-root --quiet ./
13 changes: 13 additions & 0 deletions tests/codemod_remove_react_import/codemod_remove_react_import.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
>>> removed.js (#changes: 1)


type Foo = React.AbstractComponent<{}>;
<div />;

>>> Launching report...

Stats:
Files changed: 1
Removed instances: 1
Still useful instances: 3

4 changes: 4 additions & 0 deletions tests/codemod_remove_react_import/removed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as React from 'react';

type Foo = React.AbstractComponent<{}>;
<div />;
3 changes: 3 additions & 0 deletions tests/codemod_remove_react_import/still_useful_1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react';

React.useState;
3 changes: 3 additions & 0 deletions tests/codemod_remove_react_import/still_useful_2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as React from 'react';

type A = typeof React.Fragment;
3 changes: 3 additions & 0 deletions tests/codemod_remove_react_import/still_useful_3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as React from 'react';

component C(ref: any) { return <div /> };

0 comments on commit eff9470

Please sign in to comment.