zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

variable_rebinding.ex (4216B)


      1 defmodule Credo.Check.Refactor.VariableRebinding do
      2   use Credo.Check,
      3     tags: [:controversial],
      4     param_defaults: [allow_bang: false],
      5     explanations: [
      6       check: """
      7       You might want to refrain from rebinding variables.
      8 
      9       Although technically fine, rebinding to the same name can lead to less
     10       precise naming.
     11 
     12       Consider this example:
     13 
     14           def find_a_good_time do
     15             time = MyApp.DateTime.now
     16             time = MyApp.DateTime.later(time, 5, :days)
     17             {:ok, time} = verify_available_time(time)
     18 
     19             time
     20           end
     21 
     22       While there is nothing wrong with this, many would consider the following
     23       implementation to be easier to comprehend:
     24 
     25           def find_a_good_time do
     26             today = DateTime.now
     27             proposed_time = DateTime.later(today, 5, :days)
     28             {:ok, verified_time} = verify_available_time(proposed_time)
     29 
     30             verified_time
     31           end
     32 
     33       In some rare cases you might really want to rebind a variable.  This can be
     34       enabled "opt-in" on a per-variable basis by setting the :allow_bang option
     35       to true and adding a bang suffix sigil to your variable.
     36 
     37           def uses_mutating_parameters(params!) do
     38             params! = do_a_thing(params!)
     39             params! = do_another_thing(params!)
     40             params! = do_yet_another_thing(params!)
     41           end
     42       """,
     43       params: [
     44         allow_bang: "Variables with a bang suffix will be ignored."
     45       ]
     46     ]
     47 
     48   @doc false
     49   @impl true
     50   def run(%SourceFile{} = source_file, params) do
     51     issue_meta = IssueMeta.for(source_file, params)
     52 
     53     Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta))
     54   end
     55 
     56   defp traverse([do: {:__block__, _, ast}], issues, {_, _, opt} = issue_meta) do
     57     variables =
     58       ast
     59       |> Enum.map(&find_assignments/1)
     60       |> List.flatten()
     61       |> Enum.filter(&only_variables(&1))
     62       |> Enum.reject(&bang_sigil(&1, opt[:allow_bang]))
     63 
     64     duplicates =
     65       variables
     66       |> Enum.filter(fn {key, _} ->
     67         Enum.count(variables, fn {other, _} -> key == other end) >= 2
     68       end)
     69       |> Enum.reverse()
     70       |> Enum.uniq_by(&get_variable_name/1)
     71 
     72     new_issues =
     73       Enum.map(duplicates, fn {variable_name, line} ->
     74         issue_for(issue_meta, Atom.to_string(variable_name), line)
     75       end)
     76 
     77     if length(new_issues) > 0 do
     78       {ast, issues ++ new_issues}
     79     else
     80       {ast, issues}
     81     end
     82   end
     83 
     84   defp traverse(ast, issues, _issue_meta) do
     85     {ast, issues}
     86   end
     87 
     88   defp find_assignments({:=, _, [lhs, _rhs]}) do
     89     find_variables(lhs)
     90   end
     91 
     92   defp find_assignments(_), do: nil
     93 
     94   defp find_variables({:=, _, args}) do
     95     Enum.map(args, &find_variables/1)
     96   end
     97 
     98   # ignore pinned variables
     99   defp find_variables({:"::", _, [{:^, _, _} | _]}) do
    100     []
    101   end
    102 
    103   defp find_variables({:"::", _, [lhs | _rhs]}) do
    104     find_variables(lhs)
    105   end
    106 
    107   # ignore pinned variables
    108   defp find_variables({:^, _, _}) do
    109     []
    110   end
    111 
    112   defp find_variables({variable_name, meta, nil}) when is_list(meta) do
    113     {variable_name, meta[:line]}
    114   end
    115 
    116   defp find_variables(tuple) when is_tuple(tuple) do
    117     tuple
    118     |> Tuple.to_list()
    119     |> find_variables
    120   end
    121 
    122   defp find_variables(list) when is_list(list) do
    123     list
    124     |> Enum.map(&find_variables/1)
    125     |> List.flatten()
    126     |> Enum.uniq_by(&get_variable_name/1)
    127   end
    128 
    129   defp find_variables(map) when is_map(map) do
    130     map
    131     |> Enum.into([])
    132     |> Enum.map(fn {_, value} -> find_variables(value) end)
    133     |> List.flatten()
    134     |> Enum.uniq_by(&get_variable_name/1)
    135   end
    136 
    137   defp find_variables(_), do: nil
    138 
    139   defp issue_for(issue_meta, trigger, line) do
    140     format_issue(
    141       issue_meta,
    142       message: "Variable \"#{trigger}\" was declared more than once.",
    143       trigger: trigger,
    144       line_no: line
    145     )
    146   end
    147 
    148   defp get_variable_name({name, _line}), do: name
    149   defp get_variable_name(nil), do: nil
    150 
    151   defp only_variables(nil), do: false
    152 
    153   defp only_variables({name, _}) do
    154     name
    155     |> Atom.to_string()
    156     |> String.starts_with?("_")
    157     |> Kernel.not()
    158   end
    159 
    160   defp bang_sigil({name, _}, allowed) do
    161     allowed &&
    162       name
    163       |> Atom.to_string()
    164       |> String.ends_with?("!")
    165   end
    166 end