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