prefer_unquoted_atoms.ex (2847B)
1 defmodule Credo.Check.Readability.PreferUnquotedAtoms do 2 use Credo.Check, 3 run_on_all: true, 4 base_priority: :high, 5 elixir_version: "< 1.7.0-dev", 6 explanations: [ 7 check: """ 8 Prefer unquoted atoms unless quotes are necessary. 9 This is helpful because a quoted atom can be easily mistaken for a string. 10 11 # preferred 12 13 :x 14 [x: 1] 15 %{x: 1} 16 17 # NOT preferred 18 19 :"x" 20 ["x": 1] 21 %{"x": 1} 22 23 The primary case where this can become an issue is when using atoms or 24 strings for keys in a Map or Keyword list. 25 26 For example, this: 27 28 %{"x": 1} 29 30 Can easily be mistaken for this: 31 32 %{"x" => 1} 33 34 Because a string key cannot be used to access a value with the equivalent 35 atom key, this can lead to subtle bugs which are hard to discover. 36 37 Like all `Readability` issues, this one is not a technical concern. 38 The code will behave identical in both ways. 39 """ 40 ] 41 42 @token_types [:atom_unsafe, :kw_identifier_unsafe] 43 44 @doc false 45 @impl true 46 # TODO: consider for experimental check front-loader (tokens) 47 def run(%SourceFile{} = source_file, params) do 48 issue_meta = IssueMeta.for(source_file, params) 49 50 source_file 51 |> Credo.Code.to_tokens() 52 |> Enum.reduce([], &find_issues(&1, &2, issue_meta)) 53 |> Enum.reverse() 54 end 55 56 for type <- @token_types do 57 defp find_issues( 58 {unquote(type), {line_no, column, _}, token}, 59 issues, 60 issue_meta 61 ) do 62 case safe_atom_name(token) do 63 nil -> 64 issues 65 66 atom -> 67 [issue_for(issue_meta, atom, line_no, column) | issues] 68 end 69 end 70 end 71 72 defp find_issues(_token, issues, _issue_meta) do 73 issues 74 end 75 76 # "safe atom" here refers to a quoted atom not containing an interpolation 77 defp safe_atom_name(token) when is_list(token) do 78 if Enum.all?(token, &is_binary/1) do 79 token 80 |> Enum.join() 81 |> safe_atom_name() 82 end 83 end 84 85 defp safe_atom_name(token) when is_binary(token) do 86 ':#{token}' 87 |> :elixir_tokenizer.tokenize(1, []) 88 |> safe_atom_name(token) 89 end 90 91 defp safe_atom_name(_), do: nil 92 93 # Elixir >= 1.6.0 94 defp safe_atom_name({:ok, [{:atom, {_, _, _}, atom} | _]}, token) do 95 if token == Atom.to_string(atom) do 96 atom 97 end 98 end 99 100 # Elixir <= 1.5.x 101 defp safe_atom_name({:ok, _, _, [{:atom, _, atom} | _]}, token) do 102 if token == Atom.to_string(atom) do 103 atom 104 end 105 end 106 107 defp issue_for(issue_meta, atom, line_no, column) do 108 trigger = ~s[:"#{atom}"] 109 110 format_issue( 111 issue_meta, 112 message: "Use unquoted atom `#{inspect(atom)}` rather than quoted atom `#{trigger}`.", 113 trigger: trigger, 114 line_no: line_no, 115 column: column 116 ) 117 end 118 end