unsafe_to_atom.ex (3032B)
1 defmodule Credo.Check.Warning.UnsafeToAtom do 2 use Credo.Check, 3 base_priority: :high, 4 category: :warning, 5 tags: [:controversial], 6 explanations: [ 7 check: """ 8 Creating atoms from unknown or external sources dynamically is a potentially 9 unsafe operation because atoms are not garbage-collected by the runtime. 10 11 Creating an atom from a string or charlist should be done by using 12 13 String.to_existing_atom(string) 14 15 or 16 17 List.to_existing_atom(charlist) 18 19 Module aliases should be constructed using 20 21 Module.safe_concat(prefix, suffix) 22 23 or 24 25 Module.safe_concat([prefix, infix, suffix]) 26 27 Jason.decode/Jason.decode! should be called using `keys: :atoms!` (*not* `keys: :atoms`): 28 29 Jason.decode(str, keys: :atoms!) 30 31 or `:keys` should be omitted (which defaults to `:strings`): 32 33 Jason.decode(str) 34 35 """ 36 ] 37 38 @doc false 39 @impl true 40 def run(%SourceFile{} = source_file, params) do 41 issue_meta = IssueMeta.for(source_file, params) 42 43 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 44 end 45 46 defp traverse({:@, _, _}, issues, _) do 47 {nil, issues} 48 end 49 50 defp traverse({{:., _loc, call}, meta, args} = ast, issues, issue_meta) do 51 case get_forbidden_call(call, args) do 52 {bad, suggestion} -> 53 {ast, issues_for_call(bad, suggestion, meta, issue_meta, issues)} 54 55 nil -> 56 {ast, issues} 57 end 58 end 59 60 defp traverse(ast, issues, _issue_meta) do 61 {ast, issues} 62 end 63 64 defp get_forbidden_call([:erlang, :list_to_atom], [_]) do 65 {":erlang.list_to_atom/1", ":erlang.list_to_existing_atom/1"} 66 end 67 68 defp get_forbidden_call([:erlang, :binary_to_atom], [_, _]) do 69 {":erlang.binary_to_atom/2", ":erlang.binary_to_existing_atom/2"} 70 end 71 72 defp get_forbidden_call([{:__aliases__, _, [:String]}, :to_atom], [_]) do 73 {"String.to_atom/1", "String.to_existing_atom/1"} 74 end 75 76 defp get_forbidden_call([{:__aliases__, _, [:List]}, :to_atom], [_]) do 77 {"List.to_atom/1", "List.to_existing_atom/1"} 78 end 79 80 defp get_forbidden_call([{:__aliases__, _, [:Module]}, :concat], [_]) do 81 {"Module.concat/1", "Module.safe_concat/1"} 82 end 83 84 defp get_forbidden_call([{:__aliases__, _, [:Module]}, :concat], [_, _]) do 85 {"Module.concat/2", "Module.safe_concat/2"} 86 end 87 88 defp get_forbidden_call([{:__aliases__, _, [:Jason]}, decode], args) 89 when decode in [:decode, :decode!] do 90 args 91 |> Enum.any?(fn arg -> Keyword.keyword?(arg) and Keyword.get(arg, :keys) == :atoms end) 92 |> if do 93 {"Jason.#{decode}(..., keys: :atoms)", "Jason.#{decode}(..., keys: :atoms!)"} 94 else 95 nil 96 end 97 end 98 99 defp get_forbidden_call(_, _) do 100 nil 101 end 102 103 defp issues_for_call(call, suggestion, meta, issue_meta, issues) do 104 options = [ 105 message: "Prefer #{suggestion} over #{call} to avoid creating atoms at runtime", 106 trigger: call, 107 line_no: meta[:line] 108 ] 109 110 [format_issue(issue_meta, options) | issues] 111 end 112 end