zf

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

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