lazy_logging.ex (3022B)
1 defmodule Credo.Check.Warning.LazyLogging do 2 use Credo.Check, 3 base_priority: :high, 4 elixir_version: "< 1.7.0", 5 param_defaults: [ 6 ignore: [:error, :warn, :info] 7 ], 8 explanations: [ 9 check: """ 10 Ensures laziness of Logger calls. 11 12 You will want to wrap expensive logger calls into a zero argument 13 function (`fn -> "string that gets logged" end`). 14 15 Example: 16 17 # preferred 18 19 Logger.debug fn -> 20 "This happened: \#{expensive_calculation(arg1, arg2)}" 21 end 22 23 # NOT preferred 24 # the interpolation is executed whether or not the info is logged 25 26 Logger.debug "This happened: \#{expensive_calculation(arg1, arg2)}" 27 """, 28 params: [ 29 ignore: "Do not raise an issue for these Logger calls." 30 ] 31 ] 32 33 @logger_functions [:debug, :info, :warn, :error] 34 35 @doc false 36 @impl true 37 def run(%SourceFile{} = source_file, params) do 38 issue_meta = IssueMeta.for(source_file, params) 39 # {<Logger import seen?>, <list of issues>} 40 state = {false, []} 41 42 {_, issues} = Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta), state) 43 44 issues 45 end 46 47 defp traverse( 48 {{:., _, [{:__aliases__, _, [:Logger]}, fun_name]}, meta, arguments} = ast, 49 state, 50 issue_meta 51 ) 52 when fun_name in @logger_functions do 53 issue = find_issue(fun_name, arguments, meta, issue_meta) 54 55 {ast, add_issue_to_state(state, issue)} 56 end 57 58 defp traverse( 59 {fun_name, meta, arguments} = ast, 60 {true, _issues} = state, 61 issue_meta 62 ) 63 when fun_name in @logger_functions do 64 issue = find_issue(fun_name, arguments, meta, issue_meta) 65 66 {ast, add_issue_to_state(state, issue)} 67 end 68 69 defp traverse( 70 {:import, _meta, arguments} = ast, 71 {_module_contains_import?, issues} = state, 72 _issue_meta 73 ) do 74 if logger_import?(arguments) do 75 {ast, {true, issues}} 76 else 77 {ast, state} 78 end 79 end 80 81 defp traverse(ast, state, _issue_meta) do 82 {ast, state} 83 end 84 85 defp add_issue_to_state(state, nil), do: state 86 87 defp add_issue_to_state({module_contains_import?, issues}, issue) do 88 {module_contains_import?, [issue | issues]} 89 end 90 91 defp find_issue(fun_name, arguments, meta, issue_meta) do 92 params = IssueMeta.params(issue_meta) 93 ignored_functions = Params.get(params, :ignore, __MODULE__) 94 95 unless Enum.member?(ignored_functions, fun_name) do 96 issue_for_call(arguments, meta, issue_meta) 97 end 98 end 99 100 defp issue_for_call([{:<<>>, _, [_ | _]} | _] = _args, meta, issue_meta) do 101 issue_for(issue_meta, meta[:line]) 102 end 103 104 defp issue_for_call(_args, _meta, _issue_meta) do 105 nil 106 end 107 108 defp logger_import?([{:__aliases__, _meta, [:Logger]}]), do: true 109 defp logger_import?(_), do: false 110 111 defp issue_for(issue_meta, line_no) do 112 format_issue( 113 issue_meta, 114 message: "Prefer lazy Logger calls.", 115 line_no: line_no 116 ) 117 end 118 end