diff --git a/.gitignore b/.gitignore index 3ca1a3d..cb5a81b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ ex_factor-*.tar # Temporary files for e.g. tests /tmp + +test/support/*_module.ex diff --git a/lib/ex_factor.ex b/lib/ex_factor.ex index 7be82d1..aad1691 100644 --- a/lib/ex_factor.ex +++ b/lib/ex_factor.ex @@ -3,14 +3,28 @@ defmodule ExFactor do Documentation for `ExFactor`. """ - alias ExFactor.Parser, as: P + alias ExFactor.Parser - def all_fns(input), do: P.all_functions(input) + def all_fns(input), do: Parser.all_functions(input) + @doc """ + use `mix xref` list all the callers of a given module. + """ + # @spec callers(module()) :: list(map()) def callers(mod) do - # System.cmd("mix", ["compile"], env: [{"MIX_ENV", "test"}]) - System.cmd("mix", ["xref", "callers", "#{mod}"], env: [{"MIX_ENV", "test"}]) - # |> IO.inspect(label: "") - # mix xref callers mod + System.cmd("mix", ["xref", "callers", "#{mod}"], env: [{"MIX_ENV", "test"}]) + |> elem(0) + |> String.trim() + |> String.split("\n") + |> mangle_list() + end + + defp mangle_list(["Compiling" <> _ | tail]), do: mangle_list(tail) + + defp mangle_list(list) do + Enum.map(list, fn string -> + [path, type] = String.split(string, " ") + %{filepath: path, dependency_type: type} + end) end end diff --git a/lib/ex_factor/evaluater.ex b/lib/ex_factor/evaluater.ex new file mode 100644 index 0000000..531a542 --- /dev/null +++ b/lib/ex_factor/evaluater.ex @@ -0,0 +1,27 @@ +defmodule ExFactor.Evaluater do + @moduledoc """ + Documentation for `ExFactor.Evaluater`. + """ + + alias ExFactor.Parser + + def modules_to_refactor(module, func, arity) do + module + |> ExFactor.callers() + |> Enum.map(fn %{filepath: filepath} -> + filepath + |> Parser.public_functions() + |> evaluate_ast(filepath, func, arity) + end) + |> Enum.reject(&is_nil(&1)) + end + + defp evaluate_ast({_ast, fns}, filepath, func, arity) do + fns + |> Enum.find(fn map -> map.name == func && map.arity == arity end) + |> case do + nil -> nil + _ -> filepath + end + end +end diff --git a/lib/ex_factor/extractor.ex b/lib/ex_factor/extractor.ex new file mode 100644 index 0000000..c849775 --- /dev/null +++ b/lib/ex_factor/extractor.ex @@ -0,0 +1,51 @@ +defmodule ExFactor.Extractor do + @moduledoc """ + Documentation for `ExFactor.Extractor`. + """ + alias ExFactor.Parser + + def emplace(files, opts) do + source_path = Keyword.get(opts, :source_path) + source_module = Keyword.get(opts, :source_module) + target_module = Keyword.get(opts, :target_module) + target_path = Keyword.get(opts, :target_path) + source_function = Keyword.get(opts, :source_function) + arity = Keyword.get(opts, :arity) + target_function = Keyword.get(opts, :target_function, source_function) + + Macro.underscore(source_module) + # target_path = Macro.underscore(target_module) <> ".ex" + Path.join([Mix.Project.app_path(), target_path]) + # |> IO.inspect(label: "") + + File.exists?(source_path) |> IO.inspect(label: "") + + {_ast, functions} = Parser.public_functions(source_path) + + map = Enum.find(functions, &(&1.name == source_function && &1.arity == arity)) + # |> IO.inspect(label: "source function") + + # map.ast + # |> Macro.to_string() + # |> IO.inspect(label: "source AST") + + case File.exists?(target_path) do + true -> + "somehow we need to add the fn to this file" + + _ -> + content = + quote generated: true do + defmodule unquote(target_module) do + @moduledoc false + unquote(map.ast) + end + end + |> Macro.to_string() + + # |> IO.inspect(label: "quoted") + + File.write(target_path, content) + end + end +end diff --git a/lib/ex_factor/parser.ex b/lib/ex_factor/parser.ex index 0c0e3be..bdf9c17 100644 --- a/lib/ex_factor/parser.ex +++ b/lib/ex_factor/parser.ex @@ -1,11 +1,18 @@ defmodule ExFactor.Parser do @moduledoc """ - Documentation for `ExFactor`. + Documentation for `ExFactor.Parser`. """ @doc """ Identify public and private functions from a module AST. """ + def all_functions(filepath) when is_binary(filepath) do + filepath + |> File.read!() + |> Code.string_to_quoted() + |> all_functions() + end + def all_functions({:ok, _ast} = input) do {_ast, public_functions} = public_functions(input) {ast, private_functions} = private_functions(input) @@ -15,6 +22,14 @@ defmodule ExFactor.Parser do @doc """ Identify public functions from a module AST. """ + def public_functions(filepath) when is_binary(filepath) do + filepath + |> File.read!() + |> Code.string_to_quoted() + |> IO.inspect(label: "all funxs") + |> public_functions() + end + def public_functions({:ok, ast}) do Macro.postwalk(ast, [], fn node, acc -> {node, walk_ast(node, acc, :def)} @@ -24,6 +39,13 @@ defmodule ExFactor.Parser do @doc """ Identify private functions from a module AST. """ + def private_functions(filepath) when is_binary(filepath) do + filepath + |> File.read!() + |> Code.string_to_quoted() + |> private_functions() + end + def private_functions({:ok, ast}) do # Macro.prewalk(ast, [], fn node, acc -> # # walk_ast(node, acc, :def) diff --git a/test/ex_factor/evaluater_test.exs b/test/ex_factor/evaluater_test.exs new file mode 100644 index 0000000..4b65f89 --- /dev/null +++ b/test/ex_factor/evaluater_test.exs @@ -0,0 +1,11 @@ +defmodule ExFactor.EvaluaterTest do + use ExUnit.Case + alias ExFactor.Evaluater + + describe "modules_to_refactor/1" do + test "it should report callers of a module function" do + assert ["test/support/support.ex" | _] = + Evaluater.modules_to_refactor(ExFactor.Parser, :all_functions, 1) + end + end +end diff --git a/test/ex_factor/extractor_test.exs b/test/ex_factor/extractor_test.exs new file mode 100644 index 0000000..2d5ae3a --- /dev/null +++ b/test/ex_factor/extractor_test.exs @@ -0,0 +1,75 @@ +defmodule ExFactor.ExtractorTest do + use ExUnit.Case + alias ExFactor.Extractor + + setup do + File.rm("test/support/source_module.ex") + File.rm("test/support/target_module.ex") + :ok + end + + test "write a new file with the function" do + content = """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + def pub1(arg1) do + :ok + end + end + """ + + File.write("test/support/source_module.ex", content) + + # target_path = Macro.underscore(target_module) + target_path = "test/support/target_module.ex" + File.rm(target_path) + + opts = [ + target_path: target_path, + target_module: ExFactor.NewMod, + source_module: ExFactorSampleModule, + source_path: "test/support/source_module.ex", + source_function: :pub1, + arity: 1 + ] + + path = Path.join([Mix.Project.app_path(), "lib", target_path <> ".ex"]) + Extractor.emplace(["test/support/source_module.ex"], opts) + + file = File.read!(target_path) |> IO.inspect(label: "target_path") + assert file =~ "def(pub1(arg1))" + assert file =~ "defmodule(ExFactor.NewMod) do" + end + + test "write a new file with the function, infer some defaults" do + content = """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + def pub1(arg1) do + :ok + end + end + """ + + File.write("test/support/source_module.ex", content) + target_path = "test/support/target_module.ex" + + opts = [ + target_path: target_path, + target_module: ExFactor.NewMod, + source_module: ExFactorSampleModule, + source_path: "test/support/source_module.ex", + source_function: :pub1, + arity: 1 + ] + + path = Path.join([Mix.Project.app_path(), "lib", target_path <> ".ex"]) + Extractor.emplace(["test/support/source_module.ex"], opts) + + file = File.read!(target_path) |> IO.inspect(label: "target_path") + assert file =~ "def(pub1(arg1))" + assert file =~ "defmodule(ExFactor.NewMod) do" + end +end diff --git a/test/ex_factor/parser_test.exs b/test/ex_factor/parser_test.exs index 181c34d..fce2056 100644 --- a/test/ex_factor/parser_test.exs +++ b/test/ex_factor/parser_test.exs @@ -2,117 +2,208 @@ defmodule ExFactor.ParserTest do use ExUnit.Case alias ExFactor.Parser - test "it should report public fns and their arity" do - {_ast, [f1]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok - end - end - """ - |> Code.string_to_quoted() - |> Parser.public_functions() + setup_all do + File.mkdir_p("test/tmp") - assert f1.name == :pub1 - assert f1.arity == 1 + on_exit(fn -> + File.rm_rf("test/tmp") + end) end - test "it should report TWO public fns" do - {_ast, [f1, f2]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok + describe "public_functions/1" do + test "it reports public fns for a filepath" do + content = """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + def pub1(arg1) do + :ok + end end + """ - def pub2(arg2) do - :yes - end + File.write("test/tmp/test_module.ex", content) + {_ast, [f1]} = Parser.public_functions("test/tmp/test_module.ex") - defp pub3(arg3) do - :private - end + assert f1.name == :pub1 + assert f1.arity == 1 end - """ - |> Code.string_to_quoted() - |> Parser.public_functions() - assert f2.name == :pub1 - assert f1.name == :pub2 - end - test "it should report private fns and arity" do - {_ast, [f1]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - defp priv1(arg1, arg2) do - :ok - end + test "it should report public fns and their arity" do + {_ast, [f1]} = + """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + def pub1(arg1) do + :ok + end + end + """ + |> Code.string_to_quoted() + |> Parser.public_functions() + + assert f1.name == :pub1 + assert f1.arity == 1 + end + + test "it should report TWO public fns" do + {_ast, [f1, f2]} = + """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + def pub1(arg1) do + :ok + end + + def pub2(arg2) do + :yes + end + + defp pub3(arg3) do + :private + end + end + """ + |> Code.string_to_quoted() + |> Parser.public_functions() + + assert f2.name == :pub1 + assert f1.name == :pub2 end - """ - |> Code.string_to_quoted() - |> Parser.private_functions() - assert f1.name == :priv1 - assert f1.arity == 2 - assert f1.defn == :defp + # test "ast references private fns and external fns called by a public fn" do + # content = """ + # defmodule ExFactorOtherModule do + # def other_pub1(arg1), do: arg1 + # end + # """ + # File.write("test/support/other_module.ex", content) + + # {ast, fns} = """ + # defmodule ExFactorSampleModule do + # def pub1(arg1) do + # arg1 + # |> ExFactorOtherModule.other_pub1() + # |> priv2() + # end + + # defp priv2(arg2) do + # :private + # end + # end + # """ + # |> Code.string_to_quoted() + # |> Parser.public_functions() + # |> IO.inspect(label: "") + # # assert f2.name == :pub1 + # # assert f1.name == :pub2 + # File.rm("test/support/other_module.ex") + # end end - test "it should report TWO private fns" do - {_ast, [f1, f2]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok - end + describe "private_functions/1" do + test "it reports private fns for a filepath" do + content = """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + defp priv1(arg1) do + :ok + end - def pub2(arg2) do - :yes + def pub1(arg1) do + :ok + end end + """ - defp priv3(arg3) do - :private - end + File.mkdir_p("test/tmp") + File.write("test/tmp/test_module.ex", content) + {_ast, [f1]} = Parser.private_functions("test/tmp/test_module.ex") - defp priv4(arg4) do - :private - end + assert f1.name == :priv1 + assert f1.arity == 1 end - """ - |> Code.string_to_quoted() - |> Parser.private_functions() - assert f2.name == :priv3 - assert f1.name == :priv4 - end - test "it should report all fns" do - {_ast, [f1, f2, f3]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok - end + test "it should report private fns and arity" do + {_ast, [f1]} = + """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + defp priv1(arg1, arg2) do + :ok + end + end + """ + |> Code.string_to_quoted() + |> Parser.private_functions() - def pub2(arg2) do - :yes - end + assert f1.name == :priv1 + assert f1.arity == 2 + assert f1.defn == :defp + end - defp priv3(arg3_1, arg3_2, arg3_3) do - :private - end + test "it should report TWO private fns" do + {_ast, [f1, f2]} = + """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + def pub1(arg1) do + :ok + end + + def pub2(arg2) do + :yes + end + + defp priv3(arg3) do + :private + end + + defp priv4(arg4) do + :private + end + end + """ + |> Code.string_to_quoted() + |> Parser.private_functions() + + assert f2.name == :priv3 + assert f1.name == :priv4 end - """ - |> Code.string_to_quoted() - |> Parser.all_functions() - assert f2.name == :pub1 - assert f1.name == :pub2 - assert f3.name == :priv3 - assert f3.defn == :defp - assert f3.arity == 3 end + describe "all_functions/1" do + test "it should report all fns" do + {_ast, [f1, f2, f3]} = + """ + defmodule ExFactorSampleModule do + @somedoc "This is somedoc" + # no aliases + def pub1(arg1) do + :ok + end + + def pub2(arg2) do + :yes + end + + defp priv3(arg3_1, arg3_2, arg3_3) do + :private + end + end + """ + |> Code.string_to_quoted() + |> Parser.all_functions() + + assert f2.name == :pub1 + assert f1.name == :pub2 + assert f3.name == :priv3 + assert f3.defn == :defp + assert f3.arity == 3 + end + end end diff --git a/test/ex_factor_test.exs b/test/ex_factor_test.exs index ff58ecf..11a281f 100644 --- a/test/ex_factor_test.exs +++ b/test/ex_factor_test.exs @@ -1,118 +1,13 @@ defmodule ExFactorTest do use ExUnit.Case - alias ExFactor.Parser - test "it should report public fns and their arity" do - {_ast, [f1]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok - end - end - """ - |> Code.string_to_quoted() - |> Parser.public_functions() - - assert f1.name == :pub1 - assert f1.arity == 1 - end + describe "callers/1" do + test "it should report callers of a module" do + [one, _three, two] = ExFactor.callers(ExFactor.Parser) - test "it should report TWO public fns" do - {_ast, [f1, f2]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok - end - - def pub2(arg2) do - :yes - end - - defp pub3(arg3) do - :private - end + assert one.dependency_type == "(runtime)" + assert one.filepath == "lib/ex_factor.ex" + assert two.filepath == "test/support/support.ex" end - """ - |> Code.string_to_quoted() - |> Parser.public_functions() - assert f2.name == :pub1 - assert f1.name == :pub2 end - - test "it should report private fns and arity" do - {_ast, [f1]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - defp priv1(arg1, arg2) do - :ok - end - end - """ - |> Code.string_to_quoted() - |> Parser.private_functions() - - assert f1.name == :priv1 - assert f1.arity == 2 - assert f1.defn == :defp - end - - test "it should report TWO private fns" do - {_ast, [f1, f2]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok - end - - def pub2(arg2) do - :yes - end - - defp priv3(arg3) do - :private - end - - defp priv4(arg4) do - :private - end - end - """ - |> Code.string_to_quoted() - |> Parser.private_functions() - assert f2.name == :priv3 - assert f1.name == :priv4 - end - - test "it should report all fns" do - {_ast, [f1, f2, f3]} = """ - defmodule CredoSampleModule do - @somedoc "This is somedoc" - # no aliases - def pub1(arg1) do - :ok - end - - def pub2(arg2) do - :yes - end - - defp priv3(arg3_1, arg3_2, arg3_3) do - :private - end - end - """ - |> Code.string_to_quoted() - |> Parser.all_functions() - assert f2.name == :pub1 - assert f1.name == :pub2 - assert f3.name == :priv3 - assert f3.defn == :defp - assert f3.arity == 3 - end - end diff --git a/test/support/support.ex b/test/support/support.ex index 7f6d13c..f95d14a 100644 --- a/test/support/support.ex +++ b/test/support/support.ex @@ -3,9 +3,9 @@ defmodule ExFactor.Support do Support moduel for `ExFactor` testing. """ - alias ExFactor.Parser + # use alias as: to verify the caller is found. + alias ExFactor.Parser, as: P def callers(mod), do: ExFactor.callers(mod) - def all_functions(input), do: Parser.all_functions(input) + def all_functions(input), do: P.all_functions(input) end -