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