zf

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

module_dependencies.ex (4174B)


      1 defmodule Credo.Check.Refactor.ModuleDependencies do
      2   use Credo.Check,
      3     base_priority: :normal,
      4     tags: [:controversial],
      5     param_defaults: [
      6       max_deps: 10,
      7       dependency_namespaces: [],
      8       excluded_namespaces: [],
      9       excluded_paths: [~r"/test/", ~r"^test/"]
     10     ],
     11     explanations: [
     12       check: """
     13       This module might be doing too much. Consider limiting the number of
     14       module dependencies.
     15 
     16       As always: This is just a suggestion. Check the configuration options for
     17       tweaking or disabling this check.
     18       """,
     19       params: [
     20         max_deps: "Maximum number of module dependencies.",
     21         dependency_namespaces: "List of dependency namespaces to include in this check",
     22         excluded_namespaces: "List of namespaces to exclude from this check",
     23         excluded_paths: "List of paths or regex to exclude from this check"
     24       ]
     25     ]
     26 
     27   alias Credo.Code.Module
     28   alias Credo.Code.Name
     29 
     30   @doc false
     31   @impl true
     32   def run(%SourceFile{} = source_file, params) do
     33     issue_meta = IssueMeta.for(source_file, params)
     34 
     35     max_deps = Params.get(params, :max_deps, __MODULE__)
     36     dependency_namespaces = Params.get(params, :dependency_namespaces, __MODULE__)
     37     excluded_namespaces = Params.get(params, :excluded_namespaces, __MODULE__)
     38     excluded_paths = Params.get(params, :excluded_paths, __MODULE__)
     39 
     40     case ignore_path?(source_file.filename, excluded_paths) do
     41       true ->
     42         []
     43 
     44       false ->
     45         Credo.Code.prewalk(
     46           source_file,
     47           &traverse(
     48             &1,
     49             &2,
     50             issue_meta,
     51             dependency_namespaces,
     52             excluded_namespaces,
     53             max_deps
     54           )
     55         )
     56     end
     57   end
     58 
     59   # Check if analyzed module path is within ignored paths
     60   defp ignore_path?(filename, excluded_paths) do
     61     directory = Path.dirname(filename)
     62 
     63     Enum.any?(excluded_paths, &matches?(directory, &1))
     64   end
     65 
     66   defp matches?(directory, %Regex{} = regex), do: Regex.match?(regex, directory)
     67   defp matches?(directory, path) when is_binary(path), do: String.starts_with?(directory, path)
     68 
     69   defp traverse(
     70          {:defmodule, meta, [mod | _]} = ast,
     71          issues,
     72          issue_meta,
     73          dependency_namespaces,
     74          excluded_namespaces,
     75          max
     76        ) do
     77     module_name = Name.full(mod)
     78 
     79     new_issues =
     80       if has_namespace?(module_name, excluded_namespaces) do
     81         []
     82       else
     83         module_dependencies = get_dependencies(ast, dependency_namespaces)
     84 
     85         issues_for_module(module_dependencies, max, issue_meta, meta)
     86       end
     87 
     88     {ast, issues ++ new_issues}
     89   end
     90 
     91   defp traverse(ast, issues, _issues_meta, _dependency_namespaces, _excluded_namespaces, _max) do
     92     {ast, issues}
     93   end
     94 
     95   defp get_dependencies(ast, dependency_namespaces) do
     96     aliases = Module.aliases(ast)
     97 
     98     ast
     99     |> Module.modules()
    100     |> with_fullnames(aliases)
    101     |> filter_namespaces(dependency_namespaces)
    102   end
    103 
    104   defp issues_for_module(deps, max_deps, issue_meta, meta) when length(deps) > max_deps do
    105     [
    106       format_issue(
    107         issue_meta,
    108         message: "Module has too many dependencies: #{length(deps)} (max is #{max_deps})",
    109         trigger: deps,
    110         line_no: meta[:line],
    111         column_no: meta[:column]
    112       )
    113     ]
    114   end
    115 
    116   defp issues_for_module(_, _, _, _), do: []
    117 
    118   # Resolve dependencies to full module names
    119   defp with_fullnames(dependencies, aliases) do
    120     dependencies
    121     |> Enum.map(&full_name(&1, aliases))
    122     |> Enum.uniq()
    123   end
    124 
    125   # Keep only dependencies which are within specified namespaces
    126   defp filter_namespaces(dependencies, namespaces) do
    127     Enum.filter(dependencies, &keep?(&1, namespaces))
    128   end
    129 
    130   defp keep?(_module_name, []), do: true
    131 
    132   defp keep?(module_name, namespaces), do: has_namespace?(module_name, namespaces)
    133 
    134   defp has_namespace?(module_name, namespaces) do
    135     Enum.any?(namespaces, &String.starts_with?(module_name, &1))
    136   end
    137 
    138   # Get full module name from list of aliases (if present)
    139   defp full_name(dep, aliases) do
    140     aliases
    141     |> Enum.find(&String.ends_with?(&1, dep))
    142     |> case do
    143       nil -> dep
    144       full_name -> full_name
    145     end
    146   end
    147 end