unused_function_return_helper.ex (7994B)
1 defmodule Credo.Check.Warning.UnusedFunctionReturnHelper do 2 @moduledoc false 3 4 # Finds candidates and then postwalks the AST to either VERIFY or FALSIFY 5 # the candidates (the acc is used to keep state). 6 7 @def_ops [:def, :defp, :defmacro] 8 @block_ops_with_head_expr [:if, :unless, :case, :for, :quote] 9 10 alias Credo.Code.Block 11 alias Credo.SourceFile 12 13 def find_unused_calls(%SourceFile{} = source_file, _params, required_mod_list, fun_names) do 14 Credo.Code.prewalk(source_file, &traverse_defs(&1, &2, required_mod_list, fun_names)) 15 end 16 17 for op <- @def_ops do 18 defp traverse_defs({unquote(op), _meta, arguments} = ast, acc, mod_list, fun_names) 19 when is_list(arguments) do 20 candidates = Credo.Code.prewalk(ast, &find_candidates(&1, &2, mod_list, fun_names)) 21 22 if Enum.any?(candidates) do 23 {nil, acc ++ filter_unused_calls(ast, candidates)} 24 else 25 {ast, acc} 26 end 27 end 28 end 29 30 defp traverse_defs(ast, acc, _, _) do 31 {ast, acc} 32 end 33 34 # 35 36 defp find_candidates( 37 {{:., _, [{:__aliases__, _, mods}, _fun_name]}, _, _} = ast, 38 acc, 39 required_mod_list, 40 nil 41 ) do 42 if mods == required_mod_list do 43 {ast, acc ++ [ast]} 44 else 45 {ast, acc} 46 end 47 end 48 49 defp find_candidates( 50 {{:., _, [{:__aliases__, _, mods}, fun_name]}, _, _} = ast, 51 acc, 52 required_mod_list, 53 restrict_fun_names 54 ) do 55 if mods == required_mod_list and fun_name in restrict_fun_names do 56 {ast, acc ++ [ast]} 57 else 58 {ast, acc} 59 end 60 end 61 62 defp find_candidates(ast, acc, _, _) do 63 {ast, acc} 64 end 65 66 # 67 68 defp filter_unused_calls(ast, candidates) do 69 candidates 70 |> Enum.map(&detect_unused_call(&1, ast)) 71 |> Enum.reject(&is_nil/1) 72 end 73 74 defp detect_unused_call(candidate, ast) do 75 ast 76 |> Credo.Code.postwalk(&traverse_verify_candidate(&1, &2, candidate), :not_verified) 77 |> verified_or_unused_call(candidate) 78 end 79 80 defp verified_or_unused_call(:VERIFIED, _), do: nil 81 defp verified_or_unused_call(_, candidate), do: candidate 82 83 # 84 85 defp traverse_verify_candidate(ast, acc, candidate) do 86 if Credo.Code.contains_child?(ast, candidate) do 87 verify_candidate(ast, acc, candidate) 88 else 89 {ast, acc} 90 end 91 end 92 93 # we know that `candidate` is part of `ast` 94 95 for op <- @def_ops do 96 defp verify_candidate({unquote(op), _, arguments} = ast, :not_verified = _acc, candidate) 97 when is_list(arguments) do 98 # IO.inspect(ast, label: "#{unquote(op)} (#{Macro.to_string(candidate)} #{acc})") 99 100 if last_call_in_do_block?(ast, candidate) || last_call_in_rescue_block?(ast, candidate) || 101 last_call_in_catch_block?(ast, candidate) do 102 {nil, :VERIFIED} 103 else 104 {nil, :FALSIFIED} 105 end 106 end 107 end 108 109 defp last_call_in_do_block?(ast, candidate) do 110 ast 111 |> Block.calls_in_do_block() 112 |> List.last() 113 |> Credo.Code.contains_child?(candidate) 114 end 115 116 defp last_call_in_rescue_block?(ast, candidate) do 117 ast 118 |> Block.calls_in_rescue_block() 119 |> List.last() 120 |> Credo.Code.contains_child?(candidate) 121 end 122 123 defp last_call_in_catch_block?(ast, candidate) do 124 ast 125 |> Block.calls_in_catch_block() 126 |> List.last() 127 |> Credo.Code.contains_child?(candidate) 128 end 129 130 for op <- @block_ops_with_head_expr do 131 defp verify_candidate({unquote(op), _, arguments} = ast, :not_verified = acc, candidate) 132 when is_list(arguments) do 133 # IO.inspect(ast, label: "#{unquote(op)} (#{Macro.to_string(candidate)} #{acc})") 134 135 head_expression = Enum.slice(arguments, 0..-2) 136 137 if Credo.Code.contains_child?(head_expression, candidate) do 138 {nil, :VERIFIED} 139 else 140 {ast, acc} 141 end 142 end 143 end 144 145 defp verify_candidate({:=, _, _} = ast, :not_verified = acc, candidate) do 146 # IO.inspect(ast, label: ":= (#{Macro.to_string(candidate)} #{acc})") 147 148 if Credo.Code.contains_child?(ast, candidate) do 149 {nil, :VERIFIED} 150 else 151 {ast, acc} 152 end 153 end 154 155 defp verify_candidate( 156 {:__block__, _, arguments} = ast, 157 :not_verified = acc, 158 candidate 159 ) 160 when is_list(arguments) do 161 # IO.inspect(ast, label: ":__block__ (#{Macro.to_string(candidate)} #{acc})") 162 163 last_call = List.last(arguments) 164 165 if Credo.Code.contains_child?(last_call, candidate) do 166 {ast, acc} 167 else 168 {nil, :FALSIFIED} 169 end 170 end 171 172 defp verify_candidate( 173 {:|>, _, arguments} = ast, 174 :not_verified = acc, 175 candidate 176 ) do 177 # IO.inspect(ast, label: ":__block__ (#{Macro.to_string(candidate)} #{acc})") 178 179 last_call = List.last(arguments) 180 181 if Credo.Code.contains_child?(last_call, candidate) do 182 {ast, acc} 183 else 184 {nil, :VERIFIED} 185 end 186 end 187 188 defp verify_candidate({:->, _, arguments} = ast, :not_verified = acc, _candidate) 189 when is_list(arguments) do 190 # IO.inspect(ast, label: ":-> (#{Macro.to_string(ast)} #{acc})") 191 192 {ast, acc} 193 end 194 195 defp verify_candidate({:fn, _, arguments} = ast, :not_verified = acc, _candidate) 196 when is_list(arguments) do 197 {ast, acc} 198 end 199 200 defp verify_candidate( 201 {:try, _, arguments} = ast, 202 :not_verified = acc, 203 candidate 204 ) 205 when is_list(arguments) do 206 # IO.inspect(ast, label: "try (#{Macro.to_string(candidate)} #{acc})") 207 208 after_block = Block.after_block_for!(ast) 209 210 if after_block && Credo.Code.contains_child?(after_block, candidate) do 211 {nil, :FALSIFIED} 212 else 213 {ast, acc} 214 end 215 end 216 217 # my_fun() 218 defp verify_candidate( 219 {fun_name, _, arguments} = ast, 220 :not_verified = acc, 221 candidate 222 ) 223 when is_atom(fun_name) and is_list(arguments) do 224 # IO.inspect(ast, label: "my_fun() (#{Macro.to_string(candidate)} #{acc})") 225 226 if Credo.Code.contains_child?(arguments, candidate) do 227 {nil, :VERIFIED} 228 else 229 {ast, acc} 230 end 231 end 232 233 # module.my_fun() 234 defp verify_candidate( 235 {{:., _, [{module, _, []}, fun_name]}, _, arguments} = ast, 236 :not_verified = acc, 237 candidate 238 ) 239 when is_atom(fun_name) and is_atom(module) and is_list(arguments) do 240 # IO.inspect(ast, label: "Mod.fun() /1 (#{Macro.to_string(candidate)} #{acc})") 241 242 if Credo.Code.contains_child?(arguments, candidate) do 243 {nil, :VERIFIED} 244 else 245 {ast, acc} 246 end 247 end 248 249 # :erlang_module.my_fun() 250 defp verify_candidate( 251 {{:., _, [module, fun_name]}, _, arguments} = ast, 252 :not_verified = acc, 253 candidate 254 ) 255 when is_atom(fun_name) and is_atom(module) and is_list(arguments) do 256 # IO.inspect(ast, label: "Mod.fun() /2 (#{Macro.to_string(candidate)} #{acc})") 257 258 if Credo.Code.contains_child?(arguments, candidate) do 259 {nil, :VERIFIED} 260 else 261 {ast, acc} 262 end 263 end 264 265 # MyModule.my_fun() 266 defp verify_candidate( 267 {{:., _, [{:__aliases__, _, mods}, fun_name]}, _, arguments} = ast, 268 :not_verified = acc, 269 candidate 270 ) 271 when is_atom(fun_name) and is_list(mods) and is_list(arguments) do 272 # IO.inspect(ast, label: "Mod.fun() /3 (#{Macro.to_string(candidate)} #{acc})") 273 274 if Credo.Code.contains_child?(arguments, candidate) do 275 {nil, :VERIFIED} 276 else 277 {ast, acc} 278 end 279 end 280 281 # module.my_fun() 282 defp verify_candidate( 283 {{:., _, [{module_variable, _, nil}, fun_name]}, _, arguments} = ast, 284 :not_verified = acc, 285 candidate 286 ) 287 when is_atom(fun_name) and is_atom(module_variable) and is_list(arguments) do 288 # IO.inspect(ast, label: "Mod.fun() /4 (#{Macro.to_string(candidate)} #{acc})") 289 290 if Credo.Code.contains_child?(arguments, candidate) do 291 {nil, :VERIFIED} 292 else 293 {ast, acc} 294 end 295 end 296 297 defp verify_candidate(ast, acc, _candidate) do 298 # IO.inspect(ast, label: "_ (#{Macro.to_string(candidate)} #{acc})") 299 300 {ast, acc} 301 end 302 end