negated_conditions_with_else.ex (2526B)
1 defmodule Credo.Check.Refactor.NegatedConditionsWithElse do 2 use Credo.Check, 3 base_priority: :high, 4 explanations: [ 5 check: """ 6 An `if` block with a negated condition should not contain an else block. 7 8 So while this is fine: 9 10 if not allowed? do 11 raise "Not allowed!" 12 end 13 14 The code in this example ... 15 16 if not allowed? do 17 raise "Not allowed!" 18 else 19 proceed_as_planned() 20 end 21 22 ... should be refactored to look like this: 23 24 if allowed? do 25 proceed_as_planned() 26 else 27 raise "Not allowed!" 28 end 29 30 The same goes for negation through `!` instead of `not`. 31 32 The reason for this is not a technical but a human one. It is easier to wrap 33 your head around a positive condition and then thinking "and else we do ...". 34 35 In the above example raising the error in case something is not allowed 36 might seem so important to put it first. But when you revisit this code a 37 while later or have to introduce a colleague to it, you might be surprised 38 how much clearer things get when the "happy path" comes first. 39 """ 40 ] 41 42 @doc false 43 @impl true 44 def run(%SourceFile{} = source_file, params) do 45 issue_meta = IssueMeta.for(source_file, params) 46 47 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 48 end 49 50 # TODO: consider for experimental check front-loader (ast) 51 defp traverse({:if, meta, arguments} = ast, issues, issue_meta) do 52 if negated_condition?(arguments) && Credo.Code.Block.else_block?(ast) do 53 new_issue = issue_for(issue_meta, meta[:line], "!") 54 55 {ast, issues ++ [new_issue]} 56 else 57 {ast, issues} 58 end 59 end 60 61 defp traverse(ast, issues, _issue_meta) do 62 {ast, issues} 63 end 64 65 defp negated_condition?(arguments) when is_list(arguments) do 66 arguments |> List.first() |> negated_condition?() 67 end 68 69 defp negated_condition?({:!, _meta, _arguments}) do 70 true 71 end 72 73 defp negated_condition?({:not, _meta, _arguments}) do 74 true 75 end 76 77 # parentheses around the condition wrap it in a __block__ 78 defp negated_condition?({:__block__, _meta, arguments}) do 79 negated_condition?(arguments) 80 end 81 82 defp negated_condition?(_) do 83 false 84 end 85 86 defp issue_for(issue_meta, line_no, trigger) do 87 format_issue( 88 issue_meta, 89 message: "Avoid negated conditions in if-else blocks.", 90 trigger: trigger, 91 line_no: line_no 92 ) 93 end 94 end