map_get_unsafe_pass.ex (2571B)
1 defmodule Credo.Check.Warning.MapGetUnsafePass do 2 use Credo.Check, 3 base_priority: :normal, 4 tags: [:controversial], 5 explanations: [ 6 check: """ 7 `Map.get/2` can lead into runtime errors if the result is passed into a pipe 8 without a proper default value. This happens when the next function in the 9 pipe cannot handle `nil` values correctly. 10 11 Example: 12 13 %{foo: [1, 2 ,3], bar: [4, 5, 6]} 14 |> Map.get(:missing_key) 15 |> Enum.each(&IO.puts/1) 16 17 This will cause a `Protocol.UndefinedError`, since `nil` isn't `Enumerable`. 18 Often times while iterating over enumerables zero iterations is preferable 19 to being forced to deal with an exception. Had there been a `[]` default 20 parameter this could have been averted. 21 22 If you are sure the value exists and can't be nil, please use `Map.fetch!/2`. 23 If you are not sure, `Map.get/3` can help you provide a safe default value. 24 """ 25 ] 26 27 @call_string "Map.get" 28 @unsafe_modules [:Enum] 29 30 @doc false 31 @impl true 32 def run(%SourceFile{} = source_file, params) do 33 issue_meta = IssueMeta.for(source_file, params) 34 35 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 36 end 37 38 defp traverse({:|>, _meta, _args} = ast, issues, issue_meta) do 39 pipe_issues = 40 ast 41 |> Macro.unpipe() 42 |> Enum.with_index() 43 |> find_pipe_issues(issue_meta) 44 45 {ast, issues ++ pipe_issues} 46 end 47 48 defp traverse(ast, issues, _issue_meta) do 49 {ast, issues} 50 end 51 52 defp find_pipe_issues(pipe, issue_meta) do 53 pipe 54 |> Enum.reduce([], fn {expr, idx}, acc -> 55 required_length = required_argument_length(idx) 56 {next_expr, _} = Enum.at(pipe, idx + 1, {nil, nil}) 57 58 case {expr, nil_safe?(next_expr)} do 59 {{{{:., meta, [{_, _, [:Map]}, :get]}, _, args}, _}, false} 60 when length(args) != required_length -> 61 acc ++ [issue_for(issue_meta, meta[:line], @call_string)] 62 63 _ -> 64 acc 65 end 66 end) 67 end 68 69 defp required_argument_length(idx) when idx == 0, do: 3 70 defp required_argument_length(_), do: 2 71 72 defp nil_safe?(expr) do 73 case expr do 74 {{{:., _, [{_, _, [module]}, _]}, _, _}, _} -> 75 !(module in @unsafe_modules) 76 77 _ -> 78 true 79 end 80 end 81 82 defp issue_for(issue_meta, line_no, trigger) do 83 format_issue( 84 issue_meta, 85 message: "Map.get with no default return value is potentially unsafe 86 in pipes, use Map.get/3 instead", 87 trigger: trigger, 88 line_no: line_no 89 ) 90 end 91 end