Current section

Files

Jump to
credo lib credo check refactor module_dependencies.ex
Raw

lib/credo/check/refactor/module_dependencies.ex

defmodule Credo.Check.Refactor.ModuleDependencies do
use Credo.Check,
id: "EX4017",
base_priority: :normal,
tags: [:controversial],
param_defaults: [
max_deps: 10,
dependency_namespaces: [],
excluded_namespaces: [],
excluded_paths: [~r"/test/", "test"]
],
explanations: [
check: """
This module might be doing too much. Consider limiting the number of
module dependencies.
As always: This is just a suggestion. Check the configuration options for
tweaking or disabling this check.
""",
params: [
max_deps: "Maximum number of module dependencies.",
dependency_namespaces: "List of dependency namespaces to include in this check",
excluded_namespaces: "List of namespaces to exclude from this check",
excluded_paths: "List of paths or regex to exclude from this check"
]
]
alias Credo.Code.Module
alias Credo.Code.Name
@doc false
@impl true
def run(%SourceFile{} = source_file, params) do
ctx = Context.build(source_file, params, __MODULE__)
if ignore_path?(source_file.filename, ctx.params.excluded_paths) do
[]
else
result = Credo.Code.prewalk(source_file, &walk/2, ctx)
result.issues
end
end
defp walk({:defmodule, meta, [mod | _]} = ast, ctx) do
module_name = Name.full(mod)
if has_namespace?(module_name, ctx.params.excluded_namespaces) do
{ast, ctx}
else
module_dependencies = get_dependencies(ast, ctx.params.dependency_namespaces)
{ast,
put_issue(
ctx,
issue_for_module(module_dependencies, ctx.params.max_deps, ctx, meta, module_name)
)}
end
end
defp walk(ast, ctx) do
{ast, ctx}
end
defp ignore_path?(filename, excluded_paths) do
directory = Path.dirname(filename)
Enum.any?(excluded_paths, &matches?(directory, &1))
end
defp matches?(directory, %Regex{} = regex), do: Regex.match?(regex, directory)
defp matches?(directory, path) when is_binary(path), do: String.starts_with?(directory, path)
defp get_dependencies(ast, dependency_namespaces) do
aliases = Module.aliases(ast)
ast
|> Module.modules()
|> with_fullnames(aliases)
|> filter_namespaces(dependency_namespaces)
end
defp with_fullnames(dependencies, aliases) do
dependencies
|> Enum.map(&full_name(&1, aliases))
|> Enum.uniq()
end
defp filter_namespaces(dependencies, namespaces) do
Enum.filter(dependencies, &keep?(&1, namespaces))
end
defp keep?(_module_name, []), do: true
defp keep?(module_name, namespaces), do: has_namespace?(module_name, namespaces)
defp has_namespace?(module_name, namespaces) do
Enum.any?(namespaces, &String.starts_with?(module_name, &1))
end
defp full_name(dep, aliases) do
aliases
|> Enum.find(&String.ends_with?(&1, dep))
|> case do
nil -> dep
full_name -> full_name
end
end
defp issue_for_module(deps, max_deps, ctx, meta, module_name) when length(deps) > max_deps do
format_issue(
ctx,
message: "Module has too many dependencies: #{length(deps)} (max is #{max_deps})",
trigger: module_name,
line_no: meta[:line]
)
end
defp issue_for_module(_, _, _, _, _), do: nil
end