negated_conditions_in_unless.ex (1765B)
1 defmodule Credo.Check.Refactor.NegatedConditionsInUnless do 2 use Credo.Check, 3 base_priority: :high, 4 explanations: [ 5 check: """ 6 Unless blocks should avoid having a negated condition. 7 8 The code in this example ... 9 10 unless !allowed? do 11 proceed_as_planned() 12 end 13 14 ... should be refactored to look like this: 15 16 if allowed? do 17 proceed_as_planned() 18 end 19 20 The reason for this is not a technical but a human one. It is pretty difficult 21 to wrap your head around a block of code that is executed if a negated 22 condition is NOT met. See what I mean? 23 """ 24 ] 25 26 @doc false 27 @impl true 28 def run(%SourceFile{} = source_file, params) do 29 issue_meta = IssueMeta.for(source_file, params) 30 31 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 32 end 33 34 defp traverse({:@, _, [{:unless, _, _}]}, issues, _issue_meta) do 35 {nil, issues} 36 end 37 38 # TODO: consider for experimental check front-loader (ast) 39 # NOTE: we have to exclude the cases matching the above clause! 40 defp traverse({:unless, _meta, arguments} = ast, issues, issue_meta) 41 when is_list(arguments) do 42 issue = issue_for_first_condition(List.first(arguments), issue_meta) 43 44 {ast, issues ++ List.wrap(issue)} 45 end 46 47 defp traverse(ast, issues, _issue_meta) do 48 {ast, issues} 49 end 50 51 defp issue_for_first_condition({:!, meta, _arguments}, issue_meta) do 52 issue_for(issue_meta, meta[:line], "!") 53 end 54 55 defp issue_for_first_condition(_, _), do: nil 56 57 defp issue_for(issue_meta, line_no, trigger) do 58 format_issue( 59 issue_meta, 60 message: "Avoid negated conditions in unless blocks.", 61 trigger: trigger, 62 line_no: line_no 63 ) 64 end 65 end