Current section

Files

Jump to
refactorex lib refactor.ex
Raw

lib/refactor.ex

defmodule Refactorex.Refactor do
alias Sourceror.Zipper
@type selection_or_line :: Macro.t() | pos_integer()
@callback can_refactor?(Zipper.t(), selection_or_line) :: :skip | true | false
@callback refactor(Zipper.t(), selection_or_line) :: Zipper.t()
defmacro __using__(attrs) do
works_on = Keyword.fetch!(attrs, :works_on)
quote do
alias Sourceror.Zipper, as: Z
alias Refactorex.Refactor.AST
@behaviour Refactorex.Refactor
@dialyzer {:no_match, available?: 2, visit: 4}
defguardp line?(selection_or_line) when is_number(selection_or_line)
def available?(_, selection_or_line)
when line?(selection_or_line) and unquote(works_on != :line),
do: false
def available?(_, selection_or_line)
when not line?(selection_or_line) and unquote(works_on != :selection),
do: false
def available?(zipper, selection_or_line) do
zipper
|> Z.traverse_while(false, &visit(&1, &2, selection_or_line, false))
|> then(fn {_, available?} -> available? end)
end
def execute(zipper, selection_or_line) do
zipper
|> Z.traverse_while(false, &visit(&1, &2, selection_or_line, true))
|> then(fn {%{node: node}, can_refactor?} -> if can_refactor?, do: node end)
end
defp visit(zipper, false, selection_or_line, refactor?) do
case can_refactor?(zipper, selection_or_line) do
:skip ->
{:skip, zipper, false}
false ->
{:cont, zipper, false}
true ->
{
:halt,
if(refactor?, do: refactor(zipper, selection_or_line), else: zipper),
true
}
end
end
def refactoring(refactored \\ nil) do
%Refactorex.Refactoring{
module: __MODULE__,
title: unquote(Keyword.fetch!(attrs, :title)),
kind: unquote(Keyword.fetch!(attrs, :kind)),
refactored: refactored
}
end
defdelegate placeholder, to: Refactorex.Refactor
end
end
@refactors [
__MODULE__.Alias.ExpandAliases,
__MODULE__.Alias.ExtractAlias,
__MODULE__.Alias.InlineAlias,
__MODULE__.Alias.MergeAliases,
__MODULE__.Alias.SortNestedAliases,
__MODULE__.Case.ConvertFromWith,
__MODULE__.Case.ConvertToWith,
__MODULE__.Constant.ExtractConstant,
__MODULE__.Constant.InlineConstant,
__MODULE__.Function.CollapseAnonymousFunction,
__MODULE__.Function.ExpandAnonymousFunction,
__MODULE__.Function.ExtractAnonymousFunction,
__MODULE__.Function.ExtractFunction,
__MODULE__.Function.InlineFunction,
__MODULE__.Function.UseKeywordSyntax,
__MODULE__.Function.UseRegularSyntax,
__MODULE__.Guard.ExtractGuard,
__MODULE__.Guard.InlineGuard,
__MODULE__.IfElse.UseKeywordSyntax,
__MODULE__.IfElse.UseRegularSyntax,
__MODULE__.Pipeline.IntroduceIOInspect,
__MODULE__.Pipeline.IntroducePipe,
__MODULE__.Pipeline.RemoveIOInspect,
__MODULE__.Pipeline.RemovePipe,
__MODULE__.Variable.ExtractVariable,
__MODULE__.Variable.InlineVariable,
__MODULE__.Variable.UnderscoreNotUsed
]
@renamers [
__MODULE__.Constant.RenameConstant,
__MODULE__.Function.RenameFunction,
__MODULE__.Guard.RenameGuard,
__MODULE__.Variable.RenameVariable
]
def available_refactorings(zipper, selection_or_line, execute? \\ false, modules \\ @refactors) do
__MODULE__
|> Task.Supervisor.async_stream_nolink(modules, fn module ->
cond do
execute? ->
if refactored = module.execute(zipper, selection_or_line),
do: module.refactoring(refactored)
module.available?(zipper, selection_or_line) ->
module.refactoring()
true ->
nil
end
end)
|> Enum.reduce([], fn
{:ok, nil}, refactorings ->
refactorings
{:ok, refactoring}, refactorings ->
[refactoring | refactorings]
{:exit, _reason}, refactorings ->
refactorings
end)
end
def refactor(zipper, selection_or_line, module) do
module = String.to_atom(module)
zipper
|> module.execute(selection_or_line)
|> Sourceror.to_string()
|> module.refactoring()
end
def rename_available?(zipper, selection),
do: length(available_refactorings(zipper, selection, false, @renamers)) == 1
def rename(zipper, selection, new_name) do
[%{module: module}] = available_refactorings(zipper, selection, false, @renamers)
zipper
|> module.execute(selection)
|> Sourceror.to_string()
|> String.replace("#{placeholder()}", new_name)
|> module.refactoring()
end
def placeholder(), do: :__y__
end