module.ex (12193B)
1 defmodule Credo.Code.Module do 2 @moduledoc """ 3 This module provides helper functions to analyse modules, return the defined 4 functions or module attributes. 5 """ 6 7 alias Credo.Code.Block 8 alias Credo.Code.Name 9 10 @type module_part :: 11 :moduledoc 12 | :shortdoc 13 | :behaviour 14 | :use 15 | :import 16 | :alias 17 | :require 18 | :module_attribute 19 | :defstruct 20 | :opaque 21 | :type 22 | :typep 23 | :callback 24 | :macrocallback 25 | :optional_callbacks 26 | :public_fun 27 | :private_fun 28 | :public_macro 29 | :private_macro 30 | :public_guard 31 | :private_guard 32 | :callback_fun 33 | :callback_macro 34 | :module 35 36 @type location :: [line: pos_integer, column: pos_integer] 37 38 @def_ops [:def, :defp, :defmacro] 39 40 @doc "Returns the list of aliases defined in a given module source code." 41 def aliases({:defmodule, _, _arguments} = ast) do 42 ast 43 |> Credo.Code.postwalk(&find_aliases/2) 44 |> Enum.uniq() 45 end 46 47 defp find_aliases({:alias, _, [{:__aliases__, _, mod_list}]} = ast, aliases) do 48 module_names = 49 mod_list 50 |> Name.full() 51 |> List.wrap() 52 53 {ast, aliases ++ module_names} 54 end 55 56 # Multi alias 57 defp find_aliases( 58 {:alias, _, 59 [ 60 {{:., _, [{:__aliases__, _, mod_list}, :{}]}, _, multi_mod_list} 61 ]} = ast, 62 aliases 63 ) do 64 module_names = 65 Enum.map(multi_mod_list, fn tuple -> 66 Name.full([Name.full(mod_list), Name.full(tuple)]) 67 end) 68 69 {ast, aliases ++ module_names} 70 end 71 72 defp find_aliases(ast, aliases) do 73 {ast, aliases} 74 end 75 76 @doc "Reads an attribute from a module's `ast`" 77 def attribute(ast, attr_name) do 78 case Credo.Code.postwalk(ast, &find_attribute(&1, &2, attr_name), {:error, nil}) do 79 {:ok, value} -> 80 value 81 82 error -> 83 error 84 end 85 end 86 87 defp find_attribute({:@, _meta, arguments} = ast, tuple, attribute_name) do 88 case List.first(arguments) do 89 {^attribute_name, _meta, [value]} -> 90 {:ok, value} 91 92 _ -> 93 {ast, tuple} 94 end 95 end 96 97 defp find_attribute(ast, tuple, _name) do 98 {ast, tuple} 99 end 100 101 @doc "Returns the function/macro count for the given module's AST" 102 def def_count(nil), do: 0 103 104 def def_count({:defmodule, _, _arguments} = ast) do 105 ast 106 |> Credo.Code.postwalk(&collect_defs/2) 107 |> Enum.count() 108 end 109 110 def defs(nil), do: [] 111 112 def defs({:defmodule, _, _arguments} = ast) do 113 Credo.Code.postwalk(ast, &collect_defs/2) 114 end 115 116 @doc "Returns the arity of the given function definition `ast`" 117 def def_arity(ast) 118 119 for op <- @def_ops do 120 def def_arity({unquote(op) = op, _, [{:when, _, fun_ast}, _]}) do 121 def_arity({op, nil, fun_ast}) 122 end 123 124 def def_arity({unquote(op), _, [{_fun_name, _, arguments}, _]}) 125 when is_list(arguments) do 126 Enum.count(arguments) 127 end 128 129 def def_arity({unquote(op), _, [{_fun_name, _, _}, _]}), do: 0 130 end 131 132 def def_arity(_), do: nil 133 134 @doc "Returns the name of the function/macro defined in the given `ast`" 135 for op <- @def_ops do 136 def def_name({unquote(op) = op, _, [{:when, _, fun_ast}, _]}) do 137 def_name({op, nil, fun_ast}) 138 end 139 140 def def_name({unquote(op), _, [{fun_name, _, _arguments}, _]}) 141 when is_atom(fun_name) do 142 fun_name 143 end 144 end 145 146 def def_name(_), do: nil 147 148 @doc "Returns the {fun_name, op} tuple of the function/macro defined in the given `ast`" 149 for op <- @def_ops do 150 def def_name_with_op({unquote(op) = op, _, _} = ast) do 151 {def_name(ast), op} 152 end 153 154 def def_name_with_op({unquote(op) = op, _, _} = ast, arity) do 155 if def_arity(ast) == arity do 156 {def_name(ast), op} 157 else 158 nil 159 end 160 end 161 end 162 163 def def_name_with_op(_), do: nil 164 165 @doc "Returns the name of the functions/macros for the given module's `ast`" 166 def def_names(nil), do: [] 167 168 def def_names({:defmodule, _, _arguments} = ast) do 169 ast 170 |> Credo.Code.postwalk(&collect_defs/2) 171 |> Enum.map(&def_name/1) 172 |> Enum.uniq() 173 end 174 175 @doc "Returns the name of the functions/macros for the given module's `ast`" 176 def def_names_with_op(nil), do: [] 177 178 def def_names_with_op({:defmodule, _, _arguments} = ast) do 179 ast 180 |> Credo.Code.postwalk(&collect_defs/2) 181 |> Enum.map(&def_name_with_op/1) 182 |> Enum.uniq() 183 end 184 185 @doc "Returns the name of the functions/macros for the given module's `ast` if it has the given `arity`." 186 def def_names_with_op(nil, _arity), do: [] 187 188 def def_names_with_op({:defmodule, _, _arguments} = ast, arity) do 189 ast 190 |> Credo.Code.postwalk(&collect_defs/2) 191 |> Enum.map(&def_name_with_op(&1, arity)) 192 |> Enum.reject(&is_nil/1) 193 |> Enum.uniq() 194 end 195 196 for op <- @def_ops do 197 defp collect_defs({:@, _, [{unquote(op), _, arguments} = ast]}, defs) 198 when is_list(arguments) do 199 {ast, defs -- [ast]} 200 end 201 202 defp collect_defs({unquote(op), _, arguments} = ast, defs) 203 when is_list(arguments) do 204 {ast, defs ++ [ast]} 205 end 206 end 207 208 defp collect_defs(ast, defs) do 209 {ast, defs} 210 end 211 212 @doc "Returns the list of modules used in a given module source code." 213 def modules({:defmodule, _, _arguments} = ast) do 214 ast 215 |> Credo.Code.postwalk(&find_dependent_modules/2) 216 |> Enum.uniq() 217 end 218 219 # exclude module name 220 defp find_dependent_modules( 221 {:defmodule, _, [{:__aliases__, _, mod_list}, _do_block]} = ast, 222 modules 223 ) do 224 module_names = 225 mod_list 226 |> Name.full() 227 |> List.wrap() 228 229 {ast, modules -- module_names} 230 end 231 232 # single alias 233 defp find_dependent_modules( 234 {:alias, _, [{:__aliases__, _, mod_list}]} = ast, 235 aliases 236 ) do 237 module_names = 238 mod_list 239 |> Name.full() 240 |> List.wrap() 241 242 {ast, aliases -- module_names} 243 end 244 245 # multi alias 246 defp find_dependent_modules( 247 {:alias, _, 248 [ 249 {{:., _, [{:__aliases__, _, mod_list}, :{}]}, _, multi_mod_list} 250 ]} = ast, 251 modules 252 ) do 253 module_names = 254 Enum.flat_map(multi_mod_list, fn tuple -> 255 [Name.full(mod_list), Name.full(tuple)] 256 end) 257 258 {ast, modules -- module_names} 259 end 260 261 defp find_dependent_modules({:__aliases__, _, mod_list} = ast, modules) do 262 module_names = 263 mod_list 264 |> Name.full() 265 |> List.wrap() 266 267 {ast, modules ++ module_names} 268 end 269 270 defp find_dependent_modules(ast, modules) do 271 {ast, modules} 272 end 273 274 @doc """ 275 Returns the name of a module's given ast node. 276 """ 277 def name(ast) 278 279 def name({:defmodule, _, [{:__aliases__, _, name_list}, _]}) do 280 Enum.map_join(name_list, ".", &name/1) 281 end 282 283 def name({:__MODULE__, _meta, nil}), do: "__MODULE__" 284 285 def name(atom) when is_atom(atom), do: atom |> to_string |> String.replace(~r/^Elixir\./, "") 286 287 def name(string) when is_binary(string), do: string 288 289 def name(_), do: "<Unknown Module Name>" 290 291 # TODO: write unit test 292 def exception?({:defmodule, _, [{:__aliases__, _, _name_list}, arguments]}) do 293 arguments 294 |> Block.calls_in_do_block() 295 |> Enum.any?(&defexception?/1) 296 end 297 298 def exception?(_), do: nil 299 300 defp defexception?({:defexception, _, _}), do: true 301 defp defexception?(_), do: false 302 303 @spec analyze(Macro.t()) :: [{module, [{module_part, location}]}] 304 def analyze(ast) do 305 {_ast, state} = Macro.prewalk(ast, initial_state(), &traverse_file/2) 306 module_parts(state) 307 end 308 309 defp traverse_file({:defmodule, meta, args}, state) do 310 [first_arg, [do: module_ast]] = args 311 312 state = start_module(state, meta) 313 {_ast, state} = Macro.prewalk(module_ast, state, &traverse_module/2) 314 315 module_name = module_name(first_arg) 316 this_module = {module_name, state.current_module} 317 submodules = find_inner_modules(module_name, module_ast) 318 319 state = update_in(state.modules, &(&1 ++ [this_module | submodules])) 320 {[], state} 321 end 322 323 defp traverse_file(ast, state), do: {ast, state} 324 325 defp module_name({:__aliases__, _, name_parts}) do 326 name_parts 327 |> Enum.map(fn 328 atom when is_atom(atom) -> atom 329 _other -> Unknown 330 end) 331 |> Module.concat() 332 end 333 334 defp module_name(_other), do: Unknown 335 336 defp find_inner_modules(module_name, module_ast) do 337 {_ast, definitions} = Macro.prewalk(module_ast, initial_state(), &traverse_file/2) 338 339 Enum.map( 340 definitions.modules, 341 fn {submodule_name, submodule_spec} -> 342 {Module.concat(module_name, submodule_name), submodule_spec} 343 end 344 ) 345 end 346 347 defp traverse_module(ast, state) do 348 case analyze(state, ast) do 349 nil -> traverse_deeper(ast, state) 350 state -> traverse_sibling(state) 351 end 352 end 353 354 defp traverse_deeper(ast, state), do: {ast, state} 355 defp traverse_sibling(state), do: {[], state} 356 357 # Part extractors 358 359 defp analyze(state, {:@, _meta, [{:doc, _, [value]}]}), 360 do: set_next_fun_modifier(state, if(value == false, do: :private, else: nil)) 361 362 defp analyze(state, {:@, _meta, [{:impl, _, [value]}]}), 363 do: set_next_fun_modifier(state, if(value == false, do: nil, else: :impl)) 364 365 defp analyze(state, {:@, meta, [{attribute, _, _}]}) 366 when attribute in ~w/moduledoc shortdoc behaviour type typep opaque callback macrocallback optional_callbacks/a, 367 do: add_module_element(state, attribute, meta) 368 369 defp analyze(state, {:@, _meta, [{ignore_attribute, _, _}]}) 370 when ignore_attribute in ~w/after_compile before_compile compile impl deprecated doc 371 typedoc dialyzer external_resource file on_definition on_load vsn spec/a, 372 do: state 373 374 defp analyze(state, {:@, meta, _}), 375 do: add_module_element(state, :module_attribute, meta) 376 377 defp analyze(state, {clause, meta, args}) 378 when clause in ~w/use import alias require defstruct/a and is_list(args), 379 do: add_module_element(state, clause, meta) 380 381 defp analyze(state, {clause, meta, definition}) 382 when clause in ~w/def defmacro defguard defp defmacrop defguardp/a do 383 fun_name = fun_name(definition) 384 385 if fun_name != state.last_fun_name do 386 state 387 |> add_module_element(code_type(clause, state.next_fun_modifier), meta) 388 |> Map.put(:last_fun_name, fun_name) 389 |> clear_next_fun_modifier() 390 else 391 state 392 end 393 end 394 395 defp analyze(state, {:do, _code}) do 396 # Not entering a do block, since this is possibly a custom macro invocation we can't 397 # understand. 398 state 399 end 400 401 defp analyze(state, {:defmodule, meta, _args}), 402 do: add_module_element(state, :module, meta) 403 404 defp analyze(_state, _ast), do: nil 405 406 defp fun_name([{name, _context, arity} | _]) when is_list(arity), do: {name, length(arity)} 407 defp fun_name([{name, _context, _} | _]), do: {name, 0} 408 defp fun_name(_), do: nil 409 410 defp code_type(:def, nil), do: :public_fun 411 defp code_type(:def, :impl), do: :callback_fun 412 defp code_type(:def, :private), do: :private_fun 413 defp code_type(:defp, _), do: :private_fun 414 415 defp code_type(:defmacro, nil), do: :public_macro 416 defp code_type(:defmacro, :impl), do: :callback_macro 417 defp code_type(macro, _) when macro in ~w/defmacro defmacrop/a, do: :private_macro 418 419 defp code_type(:defguard, nil), do: :public_guard 420 defp code_type(guard, _) when guard in ~w/defguard defguardp/a, do: :private_guard 421 422 # Internal state 423 424 defp initial_state, 425 do: %{modules: [], current_module: nil, next_fun_modifier: nil, last_fun_name: nil} 426 427 defp set_next_fun_modifier(state, value), do: %{state | next_fun_modifier: value} 428 429 defp clear_next_fun_modifier(state), do: set_next_fun_modifier(state, nil) 430 431 defp module_parts(state) do 432 state.modules 433 |> Enum.sort_by(fn {_name, module} -> module.location end) 434 |> Enum.map(fn {name, module} -> {name, Enum.reverse(module.parts)} end) 435 end 436 437 defp start_module(state, meta) do 438 %{state | current_module: %{parts: [], location: Keyword.take(meta, ~w/line column/a)}} 439 end 440 441 defp add_module_element(state, element, meta) do 442 location = Keyword.take(meta, ~w/line column/a) 443 update_in(state.current_module.parts, &[{element, location} | &1]) 444 end 445 end