zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

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