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