zf

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

alias_usage.ex (8360B)


      1 defmodule Credo.Check.Design.AliasUsage do
      2   use Credo.Check,
      3     base_priority: :normal,
      4     param_defaults: [
      5       excluded_namespaces: ~w[File IO Inspect Kernel Macro Supervisor Task Version],
      6       excluded_lastnames: ~w[Access Agent Application Atom Base Behaviour
      7                           Bitwise Code Date DateTime Dict Enum Exception
      8                           File Float GenEvent GenServer HashDict HashSet
      9                           Integer IO Kernel Keyword List Macro Map MapSet
     10                           Module NaiveDateTime Node OptionParser Path Port
     11                           Process Protocol Range Record Regex Registry Set
     12                           Stream String StringIO Supervisor System Task Time
     13                           Tuple URI Version],
     14       if_nested_deeper_than: 0,
     15       if_called_more_often_than: 0,
     16       only: nil
     17     ],
     18     explanations: [
     19       check: """
     20       Functions from other modules should be used via an alias if the module's
     21       namespace is not top-level.
     22 
     23       While this is completely fine:
     24 
     25           defmodule MyApp.Web.Search do
     26             def twitter_mentions do
     27               MyApp.External.TwitterAPI.search(...)
     28             end
     29           end
     30 
     31       ... you might want to refactor it to look like this:
     32 
     33           defmodule MyApp.Web.Search do
     34             alias MyApp.External.TwitterAPI
     35 
     36             def twitter_mentions do
     37               TwitterAPI.search(...)
     38             end
     39           end
     40 
     41       The thinking behind this is that you can see the dependencies of your module
     42       at a glance. So if you are attempting to build a medium to large project,
     43       this can help you to get your boundaries/layers/contracts right.
     44 
     45       As always: This is just a suggestion. Check the configuration options for
     46       tweaking or disabling this check.
     47       """,
     48       params: [
     49         excluded_namespaces: "List of namespaces to be excluded for this check.",
     50         excluded_lastnames: "List of lastnames to be excluded for this check.",
     51         if_nested_deeper_than: "Only raise an issue if a module is nested deeper than this.",
     52         if_called_more_often_than:
     53           "Only raise an issue if a module is called more often than this.",
     54         only: """
     55         Regex or a list of regexes that specifies which modules to include for this check.
     56 
     57         `excluded_namespaces` and `excluded_lastnames` take precedence over this parameter.
     58         """
     59       ]
     60     ]
     61 
     62   alias Credo.Code.Name
     63 
     64   @doc false
     65   @impl true
     66   def run(%SourceFile{} = source_file, params) do
     67     issue_meta = IssueMeta.for(source_file, params)
     68 
     69     excluded_namespaces = Params.get(params, :excluded_namespaces, __MODULE__)
     70 
     71     excluded_lastnames = Params.get(params, :excluded_lastnames, __MODULE__)
     72 
     73     if_nested_deeper_than = Params.get(params, :if_nested_deeper_than, __MODULE__)
     74 
     75     if_called_more_often_than = Params.get(params, :if_called_more_often_than, __MODULE__)
     76 
     77     only = Params.get(params, :only, __MODULE__)
     78 
     79     source_file
     80     |> Credo.Code.prewalk(
     81       &traverse(&1, &2, issue_meta, excluded_namespaces, excluded_lastnames, only)
     82     )
     83     |> filter_issues_if_called_more_often_than(if_called_more_often_than)
     84     |> filter_issues_if_nested_deeper_than(if_nested_deeper_than)
     85   end
     86 
     87   defp traverse(
     88          {:defmodule, _, _} = ast,
     89          issues,
     90          issue_meta,
     91          excluded_namespaces,
     92          excluded_lastnames,
     93          only
     94        ) do
     95     aliases = Credo.Code.Module.aliases(ast)
     96     mod_deps = Credo.Code.Module.modules(ast)
     97 
     98     new_issues =
     99       Credo.Code.prewalk(
    100         ast,
    101         &find_issues(
    102           &1,
    103           &2,
    104           issue_meta,
    105           excluded_namespaces,
    106           excluded_lastnames,
    107           only,
    108           aliases,
    109           mod_deps
    110         )
    111       )
    112 
    113     {ast, issues ++ new_issues}
    114   end
    115 
    116   defp traverse(
    117          ast,
    118          issues,
    119          _source_file,
    120          _excluded_namespaces,
    121          _excluded_lastnames,
    122          _only
    123        ) do
    124     {ast, issues}
    125   end
    126 
    127   # Ignore module attributes
    128   defp find_issues({:@, _, _}, issues, _, _, _, _, _, _) do
    129     {nil, issues}
    130   end
    131 
    132   # Ignore multi alias call
    133   defp find_issues(
    134          {:., _, [{:__aliases__, _, _}, :{}]} = ast,
    135          issues,
    136          _,
    137          _,
    138          _,
    139          _,
    140          _,
    141          _
    142        ) do
    143     {ast, issues}
    144   end
    145 
    146   # Ignore alias containing an `unquote` call
    147   defp find_issues(
    148          {:., _, [{:__aliases__, _, mod_list}, :unquote]} = ast,
    149          issues,
    150          _,
    151          _,
    152          _,
    153          _,
    154          _,
    155          _
    156        )
    157        when is_list(mod_list) do
    158     {ast, issues}
    159   end
    160 
    161   defp find_issues(
    162          {:., _, [{:__aliases__, meta, mod_list}, fun_atom]} = ast,
    163          issues,
    164          issue_meta,
    165          excluded_namespaces,
    166          excluded_lastnames,
    167          only,
    168          aliases,
    169          mod_deps
    170        )
    171        when is_list(mod_list) and is_atom(fun_atom) do
    172     cond do
    173       Enum.count(mod_list) <= 1 || Enum.any?(mod_list, &tuple?/1) ->
    174         {ast, issues}
    175 
    176       Enum.any?(mod_list, &unquote?/1) ->
    177         {ast, issues}
    178 
    179       excluded_lastname_or_namespace?(
    180         mod_list,
    181         excluded_namespaces,
    182         excluded_lastnames
    183       ) ->
    184         {ast, issues}
    185 
    186       excluded_with_only?(mod_list, only) ->
    187         {ast, issues}
    188 
    189       conflicting_with_aliases?(mod_list, aliases) ->
    190         {ast, issues}
    191 
    192       conflicting_with_other_modules?(mod_list, mod_deps) ->
    193         {ast, issues}
    194 
    195       true ->
    196         trigger = Credo.Code.Name.full(mod_list)
    197 
    198         {ast, issues ++ [issue_for(issue_meta, meta[:line], trigger)]}
    199     end
    200   end
    201 
    202   defp find_issues(ast, issues, _, _, _, _, _, _) do
    203     {ast, issues}
    204   end
    205 
    206   defp unquote?({:unquote, _, arguments}) when is_list(arguments), do: true
    207   defp unquote?(_), do: false
    208 
    209   defp excluded_lastname_or_namespace?(
    210          mod_list,
    211          excluded_namespaces,
    212          excluded_lastnames
    213        ) do
    214     first_name = Credo.Code.Name.first(mod_list)
    215     last_name = Credo.Code.Name.last(mod_list)
    216 
    217     Enum.member?(excluded_namespaces, first_name) || Enum.member?(excluded_lastnames, last_name)
    218   end
    219 
    220   defp excluded_with_only?(_mod_list, nil), do: false
    221 
    222   defp excluded_with_only?(mod_list, only) when is_list(only) do
    223     Enum.any?(only, &excluded_with_only?(mod_list, &1))
    224   end
    225 
    226   defp excluded_with_only?(mod_list, %Regex{} = only) do
    227     name = Credo.Code.Name.full(mod_list)
    228     !String.match?(name, only)
    229   end
    230 
    231   # Returns true if mod_list and alias_name would result in the same alias
    232   # since they share the same last name.
    233   defp conflicting_with_aliases?(mod_list, aliases) do
    234     last_name = Credo.Code.Name.last(mod_list)
    235 
    236     Enum.find(aliases, &conflicting_alias?(&1, mod_list, last_name))
    237   end
    238 
    239   defp conflicting_alias?(alias_name, mod_list, last_name) do
    240     full_name = Credo.Code.Name.full(mod_list)
    241     alias_last_name = Credo.Code.Name.last(alias_name)
    242 
    243     full_name != alias_name && alias_last_name == last_name
    244   end
    245 
    246   # Returns true if mod_list and any dependent module would result in the same alias
    247   # since they share the same last name.
    248   defp conflicting_with_other_modules?(mod_list, mod_deps) do
    249     full_name = Credo.Code.Name.full(mod_list)
    250     last_name = Credo.Code.Name.last(mod_list)
    251 
    252     (mod_deps -- [full_name])
    253     |> Enum.filter(&(Credo.Code.Name.parts_count(&1) > 1))
    254     |> Enum.map(&Credo.Code.Name.last/1)
    255     |> Enum.any?(&(&1 == last_name))
    256   end
    257 
    258   defp tuple?(t) when is_tuple(t), do: true
    259   defp tuple?(_), do: false
    260 
    261   defp filter_issues_if_called_more_often_than(issues, 0) do
    262     issues
    263   end
    264 
    265   defp filter_issues_if_called_more_often_than(issues, count) do
    266     issues
    267     |> Enum.reduce(%{}, fn issue, memo ->
    268       list = memo[issue.trigger] || []
    269 
    270       Map.put(memo, issue.trigger, [issue | list])
    271     end)
    272     |> Enum.filter(fn {_trigger, issues} ->
    273       length(issues) > count
    274     end)
    275     |> Enum.flat_map(fn {_trigger, issues} ->
    276       issues
    277     end)
    278   end
    279 
    280   defp filter_issues_if_nested_deeper_than(issues, count) do
    281     Enum.filter(issues, fn issue ->
    282       Name.parts_count(issue.trigger) > count
    283     end)
    284   end
    285 
    286   defp issue_for(issue_meta, line_no, trigger) do
    287     format_issue(
    288       issue_meta,
    289       message: "Nested modules could be aliased at the top of the invoking module.",
    290       trigger: trigger,
    291       line_no: line_no
    292     )
    293   end
    294 end