diff --git a/lib/ex_factor/callers.ex b/lib/ex_factor/callers.ex index e4c38f2..f95389c 100644 --- a/lib/ex_factor/callers.ex +++ b/lib/ex_factor/callers.ex @@ -12,10 +12,11 @@ defmodule ExFactor.Callers do use `mix xref` list all the callers of a given module. """ def callers(mod) do - capture_io(fn -> Mix.Tasks.Xref.run(["callers", mod]) end) - |> String.trim() - |> String.split("\n") - |> mangle_list() + Mix.Tasks.Xref.calls([]) + |> Enum.filter(fn x -> + module = cast(mod) + match?({^module, _, _}, x.callee) + end) end def callers(mod, func, arity) do @@ -53,7 +54,10 @@ defmodule ExFactor.Callers do defp mangle_list(list) do Enum.map(list, fn string -> [path, type] = String.split(string, " ") - %{filepath: path, dependency_type: type} + %{ + file: path, + dependency_type: type + } end) end end diff --git a/lib/ex_factor/changer.ex b/lib/ex_factor/changer.ex index 6d477dd..db49495 100644 --- a/lib/ex_factor/changer.ex +++ b/lib/ex_factor/changer.ex @@ -6,6 +6,21 @@ defmodule ExFactor.Changer do alias ExFactor.Callers + @doc """ + Given all the Callers of a module, find the instances of usage of the module and refactor the + module reference to the new module. Respect any existing aliases. + """ + def rename_module(opts) do + Mix.Tasks.Compile.Elixir.run([]) + :timer.sleep(100) + source_module = Keyword.fetch!(opts, :source_module) + + source_module + |> Callers.callers() + |> Enum.group_by(& &1.file) + |> update_caller_module(opts) + end + @doc """ Given all the Callers to a module, find the instances of the target function and refactor the function module reference to the new module. Respect any existing aliases. @@ -31,6 +46,24 @@ defmodule ExFactor.Changer do [%ExFactor{state: [:unchanged], message: "No additional references to source module: (#{source_module}) detected"}] end + defp update_caller_module(callers, opts) do + dry_run = Keyword.get(opts, :dry_run, false) + + Enum.map(callers, fn {file, [first | _] = grouped_callers} -> + file_list = + File.read!(file) + |> String.split("\n") + + grouped_callers + |> Enum.reduce({[:unchanged], file_list}, fn %{line: line}, acc -> + find_and_replace_module(acc, opts, line) + end) + |> maybe_add_import(opts) + |> maybe_add_alias(opts) + |> write_file(first.caller_module, file, dry_run) + end) + end + defp update_caller_groups(callers, opts) do dry_run = Keyword.get(opts, :dry_run, false) @@ -87,6 +120,43 @@ defmodule ExFactor.Changer do {new_state, List.replace_at(file_list, line - 1, new_line)} end + defp find_and_replace_module({state, file_list}, opts, line) do + # opts values + source_module = Keyword.fetch!(opts, :source_module) + target_module = Keyword.fetch!(opts, :target_module) + + # modified values + source_string = to_string(source_module) + source_modules = String.split(source_module, ".") + source_alias = Enum.at(source_modules, -1) + target_alias = preferred_alias(file_list, target_module) + source_alias_alt = find_alias_as(file_list, source_module) + fn_line = Enum.at(file_list, line - 1) + + {new_state, new_line} = + cond do + # match full module name + String.match?(fn_line, ~r/#{source_string}/) -> + fn_line = String.replace(fn_line, source_module, target_alias) + {set_state(state, :changed), fn_line} + + # match aliased module name + String.match?(fn_line, ~r/#{source_alias}/) -> + fn_line = String.replace(fn_line, source_alias, target_alias) + {set_state(state, :changed), fn_line} + + # match module name aliased :as + String.match?(fn_line, ~r/#{source_alias_alt}/) -> + fn_line = String.replace(fn_line, source_alias_alt, target_alias) + {set_state(state, :changed), fn_line} + + true -> + {state, fn_line} + end + + {new_state, List.replace_at(file_list, line - 1, new_line)} + end + defp find_alias_as(list, module) do aalias = Enum.find(list, "", fn el -> str_match?(el, module) end) diff --git a/test/ex_factor/callers_test.exs b/test/ex_factor/callers_test.exs index ead2290..2bb913e 100644 --- a/test/ex_factor/callers_test.exs +++ b/test/ex_factor/callers_test.exs @@ -4,11 +4,12 @@ defmodule ExFactor.CallersTest do describe "callers/1" do test "it should report callers of a module" do - [one, _two, _three, _four, five] = Callers.callers(ExFactor.Parser) + callers = Callers.callers(ExFactor.Parser) - assert one.dependency_type == "(runtime)" - assert one.filepath == "lib/ex_factor/callers.ex" - assert five.filepath == "test/support/support.ex" + assert Enum.find(callers, fn caller -> caller.file == "lib/ex_factor/callers.ex" end) + %{} = support = Enum.find(callers, fn caller -> caller.file == "test/support/support.ex" end) + assert support.caller_module == ExFactor.Support + assert support.callee == {ExFactor.Parser, :all_functions, 1} end test "when no callers" do diff --git a/test/ex_factor/changer_test.exs b/test/ex_factor/changer_test.exs index ad897ba..2f33ec0 100644 --- a/test/ex_factor/changer_test.exs +++ b/test/ex_factor/changer_test.exs @@ -8,6 +8,78 @@ defmodule ExFactor.ChangerTest do :ok end + describe "rename_module/1" do + test "it renames all the instances of a module" do + content = """ + defmodule ExFactor.Tmp.SourceMod do + def refactor1(_), do: :ok + end + """ + + File.write("lib/ex_factor/tmp/source_module.ex", content) + + content = """ + defmodule ExFactor.Tmp.CallerModule do + @moduledoc \"\"\" + This is a multiline moduedoc. + Its in the caller module + \"\"\" + alias ExFactor.Tmp.SourceMod + alias ExFactor.Tmp.SourceMod.Other + def pub1(arg_a) do + SourceMod.refactor1(arg_a) + end + def pub2, do: Other + + def pub3(arg_a) do + SourceMod.refactor1(arg_a) + end + end + """ + + File.write("lib/ex_factor/tmp/caller_module.ex", content) + + content = """ + defmodule ExFactor.Tmp.CallerTwoModule do + def pub1(arg_a) do + ExFactor.Tmp.SourceMod.refactor1(arg_a) + end + def pub2, do: Enum + end + """ + + File.write("lib/ex_factor/tmp/caller_two_module.ex", content) + + opts = [ + target_module: "ExFactor.Tmp.TargetModule", + source_module: "ExFactor.Tmp.SourceMod" + ] + + changes = Changer.rename_module(opts) + assert Enum.find(changes, &(&1.path == "lib/ex_factor/tmp/caller_module.ex")) + assert Enum.find(changes, &(&1.path == "lib/ex_factor/tmp/caller_two_module.ex")) + + caller = File.read!("lib/ex_factor/tmp/caller_module.ex") + assert caller =~ "alias ExFactor.Tmp.TargetModule" + # ensure we don't match dumbly + assert caller =~ "alias ExFactor.Tmp.SourceMod.Other" + refute caller =~ "alias ExFactor.Tmp.TargetModule.Other" + # assert the alias doesn't get spliced into the moduledoc + refute caller =~ "Its in the caller module\nalias ExFactor.Tmp.TargetModule\n \"" + assert caller =~ "TargetModule.refactor1(arg_a)" + # asser the function uses the alias + refute caller =~ "ExFactor.Tmp.TargetModule.refactor1(arg_a)" + assert caller =~ "def pub3(arg_a) do\n TargetModule.refactor1(arg_a)" + + caller_two = File.read!("lib/ex_factor/tmp/caller_two_module.ex") + assert caller_two =~ "alias ExFactor.Tmp.TargetModule" + # ensure we don't match dumbly + assert caller_two =~ "TargetModule.refactor1(arg_a)" + # asser the function uses the alias + refute caller_two =~ "ExFactor.Tmp.TargetModule.refactor1(arg_a)" + end + end + describe "change/1" do test "it finds all the callers of a module, function, and arity, and updates the calls to the new module" do content = """