Skip to content

Commit

Permalink
feature(Change Module): add fn to only change a module name
Browse files Browse the repository at this point in the history
Add tests and docs for the same module renamer.
  • Loading branch information
ckoch-cars committed Aug 4, 2022
1 parent 2b7b09d commit b4ec350
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 9 deletions.
14 changes: 9 additions & 5 deletions lib/ex_factor/callers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
70 changes: 70 additions & 0 deletions lib/ex_factor/changer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
9 changes: 5 additions & 4 deletions test/ex_factor/callers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions test/ex_factor/changer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand Down

0 comments on commit b4ec350

Please sign in to comment.