strict_module_layout.ex (6554B)
1 defmodule Credo.Check.Readability.StrictModuleLayout do 2 use Credo.Check, 3 base_priority: :low, 4 tags: [:controversial], 5 explanations: [ 6 check: """ 7 Provide module parts in a required order. 8 9 # preferred 10 11 defmodule MyMod do 12 @moduledoc "moduledoc" 13 use Foo 14 import Bar 15 alias Baz 16 require Qux 17 end 18 19 Like all `Readability` issues, this one is not a technical concern. 20 But you can improve the odds of others reading and liking your code by making 21 it easier to follow. 22 """, 23 params: [ 24 order: """ 25 List of atoms identifying the desired order of module parts. 26 27 Supported values are: 28 29 - `:moduledoc` - `@moduledoc` module attribute 30 - `:shortdoc` - `@shortdoc` module attribute 31 - `:behaviour` - `@behaviour` module attribute 32 - `:use` - `use` expression 33 - `:import` - `import` expression 34 - `:alias` - `alias` expression 35 - `:require` - `require` expression 36 - `:defstruct` - `defstruct` expression 37 - `:opaque` - `@opaque` module attribute 38 - `:type` - `@type` module attribute 39 - `:typep` - `@typep` module attribute 40 - `:callback` - `@callback` module attribute 41 - `:macrocallback` - `@macrocallback` module attribute 42 - `:optional_callbacks` - `@optional_callbacks` module attribute 43 - `:module_attribute` - other module attribute 44 - `:public_fun` - public function 45 - `:private_fun` - private function or a public function marked with `@doc false` 46 - `:public_macro` - public macro 47 - `:private_macro` - private macro or a public macro marked with `@doc false` 48 - `:callback_impl` - public function or macro marked with `@impl` 49 - `:public_guard` - public guard 50 - `:private_guard` - private guard or a public guard marked with `@doc false` 51 - `:module` - inner module definition (`defmodule` expression inside a module) 52 53 Notice that the desired order always starts from the top. 54 55 For example, if you provide the order `~w/public_fun private_fun/a`, 56 it means that everything else (e.g. `@moduledoc`) must appear after 57 function definitions. 58 """, 59 ignore: """ 60 List of atoms identifying the module parts which are not checked, and may 61 therefore appear anywhere in the module. Supported values are the same as 62 in the `:order` param. 63 """ 64 ] 65 ], 66 param_defaults: [ 67 order: ~w/shortdoc moduledoc behaviour use import alias require/a, 68 ignore: [] 69 ] 70 71 alias Credo.CLI.Output.UI 72 73 @doc false 74 @impl true 75 def run(%SourceFile{} = source_file, params \\ []) do 76 params = normalize_params(params) 77 78 source_file 79 |> Credo.Code.ast() 80 |> Credo.Code.Module.analyze() 81 |> all_errors(params, IssueMeta.for(source_file, params)) 82 |> Enum.sort_by(&{&1.line_no, &1.column}) 83 end 84 85 defp normalize_params(params) do 86 order = 87 params 88 |> Params.get(:order, __MODULE__) 89 |> Enum.map(fn element -> 90 # TODO: This is done for backward compatibility and should be removed in some future version. 91 with :callback_fun <- element do 92 UI.warn([ 93 :red, 94 "** (StrictModuleLayout) Check param `:callback_fun` has been deprecated. Use `:callback_impl` instead.\n\n", 95 " Use `mix credo explain #{Credo.Code.Module.name(__MODULE__)}` to learn more. \n" 96 ]) 97 98 :callback_impl 99 end 100 end) 101 102 Keyword.put(params, :order, order) 103 end 104 105 defp all_errors(modules_and_parts, params, issue_meta) do 106 expected_order = expected_order(params) 107 ignored_parts = Keyword.get(params, :ignore, []) 108 109 Enum.reduce( 110 modules_and_parts, 111 [], 112 fn {module, parts}, errors -> 113 parts = 114 parts 115 |> Stream.map(fn 116 # Converting `callback_macro` and `callback_fun` into a common `callback_impl`, 117 # because enforcing an internal order between these two kinds is counterproductive if 118 # a module implements multiple behaviours. In such cases, we typically want to group 119 # callbacks by the implementation, not by the kind (fun vs macro). 120 {callback_impl, location} when callback_impl in ~w/callback_macro callback_fun/a -> 121 {:callback_impl, location} 122 123 other -> 124 other 125 end) 126 |> Stream.reject(fn {part, _location} -> part in ignored_parts end) 127 128 module_errors(module, parts, expected_order, issue_meta) ++ errors 129 end 130 ) 131 end 132 133 defp expected_order(params) do 134 params 135 |> Keyword.fetch!(:order) 136 |> Enum.with_index() 137 |> Map.new() 138 end 139 140 defp module_errors(module, parts, expected_order, issue_meta) do 141 Enum.reduce( 142 parts, 143 %{module: module, current_part: nil, errors: []}, 144 &check_part_location(&2, &1, expected_order, issue_meta) 145 ).errors 146 end 147 148 defp check_part_location(state, {part, file_pos}, expected_order, issue_meta) do 149 state 150 |> validate_order(part, file_pos, expected_order, issue_meta) 151 |> Map.put(:current_part, part) 152 end 153 154 defp validate_order(state, part, file_pos, expected_order, issue_meta) do 155 if is_nil(state.current_part) or 156 order(state.current_part, expected_order) <= order(part, expected_order), 157 do: state, 158 else: add_error(state, part, file_pos, issue_meta) 159 end 160 161 defp order(part, expected_order), do: Map.get(expected_order, part, map_size(expected_order)) 162 163 defp add_error(state, part, file_pos, issue_meta) do 164 update_in( 165 state.errors, 166 &[error(issue_meta, part, state.current_part, state.module, file_pos) | &1] 167 ) 168 end 169 170 defp error(issue_meta, part, current_part, module, file_pos) do 171 format_issue( 172 issue_meta, 173 message: "#{part_to_string(part)} must appear before #{part_to_string(current_part)}", 174 trigger: inspect(module), 175 line_no: Keyword.get(file_pos, :line), 176 column: Keyword.get(file_pos, :column) 177 ) 178 end 179 180 defp part_to_string(:module_attribute), do: "module attribute" 181 defp part_to_string(:public_guard), do: "public guard" 182 defp part_to_string(:public_macro), do: "public macro" 183 defp part_to_string(:public_fun), do: "public function" 184 defp part_to_string(:private_fun), do: "private function" 185 defp part_to_string(:callback_impl), do: "callback implementation" 186 defp part_to_string(part), do: "#{part}" 187 end