forbidden_module.ex (3023B)
1 defmodule Credo.Check.Warning.ForbiddenModule do 2 use Credo.Check, 3 base_priority: :high, 4 category: :warning, 5 param_defaults: [modules: []], 6 explanations: [ 7 check: """ 8 Some modules that are included by a package may be hazardous 9 if used by your application. Use this check to allow these modules in 10 your dependencies but forbid them to be used in your application. 11 12 Examples: 13 14 The `:ecto_sql` package includes the `Ecto.Adapters.SQL` module, 15 but direct usage of the `Ecto.Adapters.SQL.query/4` function, and related functions, may 16 cause issues when using Ecto's dynamic repositories. 17 """, 18 params: [ 19 modules: "List of modules or `{Module, \"Error message\"}` tuples that must not be used." 20 ] 21 ] 22 23 alias Credo.Code.Name 24 25 @impl Credo.Check 26 def run(%SourceFile{} = source_file, params) do 27 modules = Params.get(params, :modules, __MODULE__) 28 29 modules = 30 if Keyword.keyword?(modules) do 31 Enum.map(modules, fn {key, value} -> {Name.full(key), value} end) 32 else 33 Enum.map(modules, fn key -> {Name.full(key), nil} end) 34 end 35 36 Credo.Code.prewalk( 37 source_file, 38 &traverse(&1, &2, modules, IssueMeta.for(source_file, params)) 39 ) 40 end 41 42 defp traverse({:__aliases__, meta, modules} = ast, issues, forbidden_modules, issue_meta) do 43 module = Name.full(modules) 44 45 issues = put_issue_if_forbidden(issues, issue_meta, meta[:line], module, forbidden_modules) 46 47 {ast, issues} 48 end 49 50 defp traverse( 51 {:alias, _meta, [{{_, _, [{:__aliases__, _opts, base_alias}, :{}]}, _, aliases}]} = ast, 52 issues, 53 forbidden_modules, 54 issue_meta 55 ) do 56 modules = 57 Enum.map(aliases, fn {:__aliases__, meta, module} -> 58 {Name.full([base_alias, module]), meta[:line]} 59 end) 60 61 issues = 62 Enum.reduce(modules, issues, fn {module, line}, issues -> 63 put_issue_if_forbidden(issues, issue_meta, line, module, forbidden_modules) 64 end) 65 66 {ast, issues} 67 end 68 69 defp traverse(ast, issues, _, _), do: {ast, issues} 70 71 defp put_issue_if_forbidden(issues, issue_meta, line_no, module, forbidden_modules) do 72 forbidden_module_names = Enum.map(forbidden_modules, &elem(&1, 0)) 73 74 if found_module?(forbidden_module_names, module) do 75 [issue_for(issue_meta, line_no, module, forbidden_modules) | issues] 76 else 77 issues 78 end 79 end 80 81 defp found_module?(forbidden_module_names, module) do 82 Enum.member?(forbidden_module_names, module) 83 end 84 85 defp issue_for(issue_meta, line_no, module, forbidden_modules) do 86 trigger = Name.full(module) 87 message = message(forbidden_modules, module) || "The `#{trigger}` module is not allowed." 88 89 format_issue( 90 issue_meta, 91 message: message, 92 trigger: trigger, 93 line_no: line_no 94 ) 95 end 96 97 defp message(forbidden_modules, module) do 98 Enum.find_value(forbidden_modules, fn 99 {^module, message} -> message 100 _ -> nil 101 end) 102 end 103 end