alias_order.ex (5751B)
1 defmodule Credo.Check.Readability.AliasOrder do 2 use Credo.Check, 3 base_priority: :low, 4 explanations: [ 5 check: """ 6 Alphabetically ordered lists are more easily scannable by the read. 7 8 # preferred 9 10 alias ModuleA 11 alias ModuleB 12 alias ModuleC 13 14 # NOT preferred 15 16 alias ModuleA 17 alias ModuleC 18 alias ModuleB 19 20 Alias should be alphabetically ordered among their group: 21 22 # preferred 23 24 alias ModuleC 25 alias ModuleD 26 27 alias ModuleA 28 alias ModuleB 29 30 # NOT preferred 31 32 alias ModuleC 33 alias ModuleD 34 35 alias ModuleB 36 alias ModuleA 37 38 Like all `Readability` issues, this one is not a technical concern. 39 But you can improve the odds of others reading and liking your code by making 40 it easier to follow. 41 """ 42 ] 43 44 alias Credo.Code.Name 45 46 @doc false 47 @impl true 48 def run(%SourceFile{} = source_file, params) do 49 issue_meta = IssueMeta.for(source_file, params) 50 51 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 52 end 53 54 defp traverse({:defmodule, _, _} = ast, issues, issue_meta) do 55 new_issues = 56 ast 57 |> extract_alias_groups() 58 |> Enum.reduce([], &traverse_groups(&1, &2, issue_meta)) 59 60 {ast, issues ++ new_issues} 61 end 62 63 defp traverse(ast, issues, _), do: {ast, issues} 64 65 defp traverse_groups(group, acc, issue_meta) do 66 group 67 |> Enum.chunk_every(2, 1) 68 |> Enum.reduce_while(nil, &process_group/2) 69 |> case do 70 nil -> 71 acc 72 73 line -> 74 acc ++ [issue_for(issue_meta, line)] 75 end 76 end 77 78 defp process_group([{line_no, mod_list_second, a}, {_line_no, _mod_list_second, b}], _) 79 when a > b do 80 module = 81 case mod_list_second do 82 {base, _} -> base 83 value -> value 84 end 85 86 issue_opts = issue_opts(line_no, module, module) 87 88 {:halt, issue_opts} 89 end 90 91 defp process_group([{line_no1, mod_list_first, _}, {line_no2, mod_list_second, _}], _) do 92 issue_opts = 93 cond do 94 issue = inner_group_order_issue(line_no1, mod_list_first) -> 95 issue 96 97 issue = inner_group_order_issue(line_no2, mod_list_second) -> 98 issue 99 100 true -> 101 nil 102 end 103 104 if issue_opts do 105 {:halt, issue_opts} 106 else 107 {:cont, nil} 108 end 109 end 110 111 defp process_group([{line_no1, mod_list_first, _}], _) do 112 if issue_opts = inner_group_order_issue(line_no1, mod_list_first) do 113 {:halt, issue_opts} 114 else 115 {:cont, nil} 116 end 117 end 118 119 defp process_group(_, _), do: {:cont, nil} 120 121 defp inner_group_order_issue(_line_no, {_base, []}), do: nil 122 123 defp inner_group_order_issue(line_no, {base, mod_list}) do 124 downcased_mod_list = Enum.map(mod_list, &String.downcase(to_string(&1))) 125 sorted_downcased_mod_list = Enum.sort(downcased_mod_list) 126 127 if downcased_mod_list != sorted_downcased_mod_list do 128 issue_opts(line_no, base, mod_list, downcased_mod_list, sorted_downcased_mod_list) 129 end 130 end 131 132 defp issue_opts(line_no, base, mod_list, downcased_mod_list, sorted_downcased_mod_list) do 133 trigger = 134 downcased_mod_list 135 |> Enum.with_index() 136 |> Enum.find_value(fn {downcased_mod_entry, index} -> 137 if downcased_mod_entry != Enum.at(sorted_downcased_mod_list, index) do 138 Enum.at(mod_list, index) 139 end 140 end) 141 142 issue_opts(line_no, [base, trigger], trigger) 143 end 144 145 defp issue_opts(line_no, module, trigger) do 146 %{ 147 line_no: line_no, 148 trigger: trigger, 149 module: module 150 } 151 end 152 153 defp extract_alias_groups({:defmodule, _, _} = ast) do 154 ast 155 |> Credo.Code.postwalk(&find_alias_groups/2) 156 |> Enum.reverse() 157 |> Enum.reduce([[]], fn definition, acc -> 158 case definition do 159 nil -> 160 [[]] ++ acc 161 162 definition -> 163 [group | groups] = acc 164 [group ++ [definition]] ++ groups 165 end 166 end) 167 |> Enum.reverse() 168 end 169 170 defp find_alias_groups( 171 {:alias, _, [{:__aliases__, meta, mod_list} | _]} = ast, 172 aliases 173 ) do 174 compare_name = compare_name(ast) 175 modules = [{meta[:line], {Name.full(mod_list), []}, compare_name}] 176 177 accumulate_alias_into_group(ast, modules, meta[:line], aliases) 178 end 179 180 defp find_alias_groups( 181 {:alias, _, 182 [ 183 {{:., _, [{:__aliases__, meta, mod_list}, :{}]}, _, multi_mod_list} 184 ]} = ast, 185 aliases 186 ) do 187 multi_mod_list = 188 multi_mod_list 189 |> Enum.map(fn {:__aliases__, _, mod_list} -> mod_name(mod_list) end) 190 191 compare_name = compare_name(ast) 192 modules = [{meta[:line], {Name.full(mod_list), multi_mod_list}, compare_name}] 193 194 nested_mod_line = meta[:line] + 1 195 accumulate_alias_into_group(ast, modules, nested_mod_line, aliases) 196 end 197 198 defp find_alias_groups(ast, aliases), do: {ast, aliases} 199 200 defp mod_name(mod_list) do 201 Enum.map_join(mod_list, ".", &to_string/1) 202 end 203 204 defp compare_name(value) do 205 value 206 |> Macro.to_string() 207 |> String.downcase() 208 |> String.replace(~r/[\{\}]/, "") 209 |> String.replace(~r/,.+/, "") 210 end 211 212 defp accumulate_alias_into_group(ast, modules, line, [{line_no, _, _} | _] = aliases) 213 when line_no != 0 and line_no != line - 1 do 214 {ast, modules ++ [nil] ++ aliases} 215 end 216 217 defp accumulate_alias_into_group(ast, modules, _, aliases) do 218 {ast, modules ++ aliases} 219 end 220 221 defp issue_for(issue_meta, %{line_no: line_no, trigger: trigger, module: module}) do 222 format_issue( 223 issue_meta, 224 message: "The alias `#{Name.full(module)}` is not alphabetically ordered among its group.", 225 trigger: trigger, 226 line_no: line_no 227 ) 228 end 229 end