Skip to content

Commit

Permalink
WIP add remove fns and modules
Browse files Browse the repository at this point in the history
  • Loading branch information
ckoch-cars committed Sep 16, 2021
1 parent eca25dd commit b319971
Show file tree
Hide file tree
Showing 12 changed files with 767 additions and 211 deletions.
7 changes: 7 additions & 0 deletions lib/ex_factor.ex
Expand Up @@ -8,4 +8,11 @@ defmodule ExFactor do
If the Module exists, it add the function to the end of the file and change all calls to the
new module's name.
"""

alias ExFactor.Extractor

@doc """
Call Extractor module emaplce/1
"""
def refactor(opts), do: Extractor.emplace(opts)
end
24 changes: 22 additions & 2 deletions lib/ex_factor/extractor.ex
Expand Up @@ -5,15 +5,20 @@ defmodule ExFactor.Extractor do
alias ExFactor.Neighbors
alias ExFactor.Parser

@doc """
Given a keyword list of opts, find the function in the specified source, refactor it, the docs,
specs, and any miscellaneous attrs proximate to the source function into the specified module.
"""

def emplace(opts) do
source_module = Keyword.get(opts, :source_module)
target_module = Keyword.get(opts, :target_module)
source_function = Keyword.get(opts, :source_function)
arity = Keyword.get(opts, :arity)
target_path = Keyword.get(opts, :target_path, path(target_module))
source_path = Keyword.get(opts, :source_path, path(source_module))
{ast, block_contents} = Parser.block_contents(source_path)
to_extract = Neighbors.prev(block_contents, source_function)
{_ast, block_contents} = Parser.block_contents(source_path)
to_extract = Neighbors.walk(block_contents, source_function, arity)

case File.exists?(target_path) do
true ->
Expand Down Expand Up @@ -41,6 +46,21 @@ defmodule ExFactor.Extractor do
end
end

def remove(opts) do
source_module = Keyword.get(opts, :source_module)
# target_module = Keyword.get(opts, :target_module)
source_function = Keyword.get(opts, :source_function)
arity = Keyword.get(opts, :arity)
# target_path = Keyword.get(opts, :target_path, path(target_module))
source_path = Keyword.get(opts, :source_path, path(source_module))
{_ast, functions} = Parser.all_functions(source_path)
functions |> IO.inspect(label: "")
# to_extract = Neighbors.walk(block_contents, source_function, arity)
# |> IO.inspect(label: "to_extract")


end

defp path(module) do
Path.join(["lib", Macro.underscore(module) <> ".ex"])
end
Expand Down
26 changes: 19 additions & 7 deletions lib/ex_factor/neighbors.ex
Expand Up @@ -3,28 +3,40 @@ defmodule ExFactor.Neighbors do
Documentation for `ExFactor.Neighbors`.
"""

def prev(block, fn_name) do
@doc """
Walk the AST and find all the elements before the target function and the previous function.
Find all the instances of the target function. Return after evaluating all the block-level
AST elements. Ignore certain elements, such as :alias.
"""
def walk(block, fn_name, arity \\ :unmatched) do
block
|> Enum.reduce({[], []}, fn el, acc ->
eval_elem(el, acc, fn_name)
eval_elem(el, acc, fn_name, arity)
end)
|> elem(1)
end

defp eval_elem({type, _, [{name, _, _} | _]} = el, {pending, acc}, name)
defp eval_elem({type, _, [{name, _, args} | _]} = el, {pending, acc}, name, arity)
when type in [:def, :defp] do
{[], acc ++ pending ++ [el]}
cond do
arity == :unmatched ->
{[], acc ++ pending ++ [el]}
length(args) == arity ->
{[], acc ++ pending ++ [el]}
true ->
{[], acc}
end
end

defp eval_elem({type, _, _}, {_pending, acc}, _name) when type in [:def, :defp] do
defp eval_elem({type, _, _}, {_pending, acc}, _name, _arity) when type in [:def, :defp] do
{[], acc}
end

defp eval_elem({type, _, _}, {pending, acc}, _name) when type in [:alias] do
defp eval_elem({type, _, _}, {pending, acc}, _name, _artiy) when type in [:alias] do
{pending, acc}
end

defp eval_elem(el, {pending, acc}, _name) do
defp eval_elem(el, {pending, acc}, _name, _artiy) do
{pending ++ [el], acc}
end
end
131 changes: 116 additions & 15 deletions lib/ex_factor/parser.ex
Expand Up @@ -3,13 +3,42 @@ defmodule ExFactor.Parser do
Documentation for `ExFactor.Parser`.
"""

# @doc """
# Identify public and private functions from a module AST.
# """
def read_file(filepath) when is_binary(filepath) do
contents = File.read!(filepath)
list = String.split(contents, "\n")
{:ok, ast} = Code.string_to_quoted(contents, token_metadata: true)
{ast, list}
end

def block_contents(filepath) when is_binary(filepath) do
filepath
|> File.read!()
|> Code.string_to_quoted(token_metadata: true)
|> block_contents()
end

def block_contents({:ok, ast}) do
# {_ast, block} =
Macro.postwalk(ast, [], fn node, acc ->
{node, ast_block(node, acc)}
end)

# Macro.postwalk(block, [], fn node, acc ->
# {node, walk_ast(node, acc, :def)}
# # |> IO.inspect(label: "walk AST")
# end)
end

@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()
|> Code.string_to_quoted(token_metadata: true)
|> all_functions()
end

Expand All @@ -25,8 +54,7 @@ defmodule ExFactor.Parser do
def public_functions(filepath) when is_binary(filepath) do
filepath
|> File.read!()
|> Code.string_to_quoted()
# |> IO.inspect(label: "all funxs")
|> Code.string_to_quoted(token_metadata: true)
|> public_functions()
end

Expand All @@ -36,37 +64,110 @@ defmodule ExFactor.Parser do
end)
end

# def public_functions({:ok, ast}) do
# ast |> IO.inspect(label: "")
# Macro.postwalk(ast, {:prev, []}, fn node, acc ->
# {node, walk_ast(node, acc, :def)}
# # |> IO.inspect(label: "walk AST")
# end)
# end

@doc """
Identify private functions from a module AST.
"""
def private_functions(filepath) when is_binary(filepath) do
filepath
|> File.read!()
|> Code.string_to_quoted()
|> Code.string_to_quoted(token_metadata: true)
|> private_functions()
end

def private_functions({:ok, ast}) do
# Macro.prewalk(ast, [], fn node, acc ->
# # walk_ast(node, acc, :def)
# {node, walk_ast(node, acc, :def)}
# # |> IO.inspect(label: "walk_ast")
# end)

Macro.postwalk(ast, [], fn node, acc ->
# walk_ast(node, acc, :def)
{node, walk_ast(node, acc, :defp)}
# |> IO.inspect(label: "walk_ast")
end)
end

defp walk_ast({tkn, _, [{name, _meta, args} | _]} = func, acc, token) when tkn == token do
defp walk_ast({:@, _, [{:doc, _meta, _} | _]} = node, acc, _token) do
map = %{name: :doc, ast: node, arity: 0, defn: "@doc"}
[map | acc]
end

defp walk_ast({:@, fn_meta, [{:spec, _meta, [{_, _, [{name, _, args} | _]} | _]} | _]} = node, acc, _token) do
arity = length(args)
map = merge_maps(%{name: name, ast: node, arity: arity, defn: "@spec"}, fn_meta)
# map =
# fn_meta
# |> IO.inspect(label: "spec meta")
# |> find_lines()
# |> Map.merge(%{name: :spec, ast: node, arity: arity, defn: "@spec"})
[map | acc]
end

defp walk_ast({tkn, fn_meta, [{:when, _when_meta, [{name, _meta, args} | _]} | _]} = node, acc, token) when tkn == token do
arity = length(args)
map = %{name: name, ast: func, arity: arity, defn: token}
map = merge_maps(%{name: name, ast: node, arity: arity, defn: token}, fn_meta)
# fn_meta
# |> find_lines()
# |> Map.merge()
[map | acc]
end

defp walk_ast(_func, acc, _token) do
defp walk_ast({tkn, fn_meta, [{name, _meta, args} | _]} = node, acc, token) when tkn == token do
arity = length(args)
map = merge_maps(%{name: name, ast: node, arity: arity, defn: token}, fn_meta)
# fn_meta
# |> find_lines()
# |> Map.merge()
[map | acc]
end

defp walk_ast(_node, acc, _token) do
# node |> IO.inspect(label: "")
acc
end

defp merge_maps(map, meta) do
meta
|> find_lines()
|> Map.merge(map)
end

defp find_lines(meta) do
start_line = Keyword.get(meta, :line, :unknown)
end_line = find_end_line(meta)
%{start_line: start_line, end_line: end_line}
end

defp find_end_line(meta) do
end_expression_line = meta
|> Keyword.get(:end_of_expression, [])
|> Keyword.get(:line, :unknown)
end_line = meta
|> Keyword.get(:end, [])
|> Keyword.get(:line, :unknown)

cond do
end_line != :unknown -> end_line
end_expression_line != :unknown -> end_expression_line
true -> :unknown
end
end

defp ast_block([do: {:__block__, [], block_contents}], _acc) do
block_contents
end

defp ast_block(_block, acc) do
acc
end
end


[{:"::", [line: 2], [
{:priv1, [closing: [line: 2], line: 2],
[{:term, [closing: [line: 2], line: 2], []}]},
{:term, [closing: [line: 2], line: 2], []}
]
}
]
49 changes: 49 additions & 0 deletions lib/ex_factor/remover.ex
@@ -0,0 +1,49 @@
defmodule ExFactor.Remover do
@moduledoc """
Documentation for `ExFactor.Remover`.
"""

# alias ExFactor.Neighbors
alias ExFactor.Parser


def remove(source_path, fn_name, arity) do
{_ast, block_contents} = Parser.all_functions(source_path)
# [function | _] = block_contents
# function = Enum.find(block_contents, &(&1.name == fn_name and &1.arity == arity && (&1.defn == :def || &1.defn == :defp)))
fns_to_remove = Enum.filter(block_contents, & &1.name == fn_name)
|> IO.inspect(label: "fns_to_remove")

{_ast, line_list} = Parser.read_file(source_path)

Enum.reduce(fns_to_remove, line_list, fn function, acc ->
delete_range = function.start_line..function.end_line
|> Enum.to_list()
|> Enum.reverse

delete_range
|> Enum.reduce(acc, fn idx, acc ->
List.delete_at(acc, idx - 1)
end)
|> List.insert_at(function.start_line, comment(fn_name, arity, function.defn))
end)
|> Enum.join("\n")
|> then(fn str -> File.write(source_path, str, [:write]) end)
end

defp comment(name, arity, "@spec") do
"""
# @spec: #{name}/#{arity} removed by ExFactor
"""
end
defp comment(name, arity, _) do
"""
#
# Function: #{name}/#{arity} removed by ExFactor
# ExFactor only removes the function itself
# Other artifacts, including docs and module-level comments
# may remain for you to remove manually.
#
"""
end
end
3 changes: 2 additions & 1 deletion mix.exs
Expand Up @@ -21,7 +21,8 @@ defmodule ExFactor.MixProject do

# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# defp elixirc_paths(_), do: ["lib"]
defp elixirc_paths(_), do: ["lib", "test"]

# Run "mix help deps" to learn about dependencies.
defp deps do
Expand Down

0 comments on commit b319971

Please sign in to comment.