parentheses_in_condition.ex (5185B)
1 defmodule Credo.Check.Readability.ParenthesesInCondition do 2 use Credo.Check, 3 base_priority: :high, 4 tags: [:formatter], 5 explanations: [ 6 check: """ 7 Because `if` and `unless` are macros, the preferred style is to not use 8 parentheses around conditions. 9 10 # preferred 11 12 if valid?(username) do 13 # ... 14 end 15 16 # NOT preferred 17 18 if( valid?(username) ) do 19 # ... 20 end 21 22 Like all `Readability` issues, this one is not a technical concern. 23 But you can improve the odds of others reading and liking your code by making 24 it easier to follow. 25 """ 26 ] 27 28 @doc false 29 @impl true 30 # TODO: consider for experimental check front-loader (tokens) 31 def run(%SourceFile{} = source_file, params) do 32 issue_meta = IssueMeta.for(source_file, params) 33 34 source_file 35 |> Credo.Code.to_tokens() 36 |> collect_parenthetical_tokens([], nil) 37 |> find_issues([], issue_meta) 38 end 39 40 defp collect_parenthetical_tokens([], acc, _), do: acc 41 42 defp collect_parenthetical_tokens([head | t], acc, prev_head) do 43 acc = 44 case check_for_opening_paren(head, t, prev_head) do 45 false -> acc 46 token -> acc ++ [token] 47 end 48 49 collect_parenthetical_tokens(t, acc, head) 50 end 51 52 defp check_for_opening_paren( 53 {:identifier, _, if_or_unless} = start, 54 [{:"(", _} = next_token | t], 55 prev_head 56 ) 57 when if_or_unless in [:if, :unless] do 58 check_for_closing_paren(start, next_token, t, prev_head) 59 end 60 61 defp check_for_opening_paren( 62 {:paren_identifier, _, if_or_unless}, 63 _, 64 {:arrow_op, _, :|>} 65 ) 66 when if_or_unless in [:if, :unless] do 67 false 68 end 69 70 defp check_for_opening_paren( 71 {:paren_identifier, _, if_or_unless} = token, 72 [{:"(", _} | t], 73 _ 74 ) 75 when if_or_unless in [:if, :unless] do 76 if Enum.any?(collect_paren_children(t), &is_do/1) do 77 false 78 else 79 token 80 end 81 end 82 83 defp check_for_opening_paren(_, _, _), do: false 84 85 # matches: if( something ) do 86 # ^^^^ 87 defp check_for_closing_paren(start, {:do, _}, _tail, {:")", _}) do 88 start 89 end 90 91 # matches: if( something ) == something_else do 92 # ^^ 93 defp check_for_closing_paren(_start, {:")", _}, [{:comp_op, _, _} | _tail], _prev_head) do 94 false 95 end 96 97 # matches: if( something ) or something_else do 98 # ^^ 99 defp check_for_closing_paren(_start, {:")", _}, [{:or_op, _, _} | _tail], _prev_head) do 100 false 101 end 102 103 # matches: if( something ) and something_else do 104 # ^^^ 105 defp check_for_closing_paren(_start, {:")", _}, [{:and_op, _, _} | _tail], _prev_head) do 106 false 107 end 108 109 # matches: if( something ) in something_else do 110 # ^^ 111 defp check_for_closing_paren(_start, {:")", _}, [{:in_op, _, _} | _tail], _prev_head) do 112 false 113 end 114 115 # matches: if( 1 + foo ) / bar > 0 do 116 # ^ 117 defp check_for_closing_paren(_start, {:")", _}, [{:mult_op, _, _} | _tail], _prev_head) do 118 false 119 end 120 121 # matches: if( 1 + foo ) + bar > 0 do 122 # ^ 123 defp check_for_closing_paren(_start, {:")", _}, [{:dual_op, _, _} | _tail], _prev_head) do 124 false 125 end 126 127 # matches: if( 1 &&& foo ) > bar do 128 # ^ 129 defp check_for_closing_paren(_start, {:")", _}, [{:rel_op, _, _} | _tail], _prev_head) do 130 false 131 end 132 133 # matches: if( something ), do: 134 # ^^ 135 defp check_for_closing_paren(start, {:",", _}, _, {:")", _}) do 136 start 137 end 138 139 defp check_for_closing_paren(_, {:or_op, _, _}, [{:"(", _} | _], _) do 140 false 141 end 142 143 defp check_for_closing_paren(_, {:and_op, _, _}, [{:"(", _} | _], _) do 144 false 145 end 146 147 defp check_for_closing_paren(_, {:comp_op, _, _}, [{:"(", _} | _], _) do 148 false 149 end 150 151 defp check_for_closing_paren(start, token, [next_token | t], _prev_head) do 152 check_for_closing_paren(start, next_token, t, token) 153 end 154 155 defp check_for_closing_paren(_, _, _, _), do: false 156 157 defp is_do({_, _, :do}), do: true 158 defp is_do(_), do: false 159 160 defp collect_paren_children(x) do 161 {_, children} = Enum.reduce(x, {0, []}, &collect_paren_child/2) 162 children 163 end 164 165 defp collect_paren_child({:"(", _}, {nest_level, tokens}), do: {nest_level + 1, tokens} 166 167 defp collect_paren_child({:")", _}, {nest_level, tokens}), do: {nest_level - 1, tokens} 168 169 defp collect_paren_child(token, {0, tokens}), do: {0, tokens ++ [token]} 170 defp collect_paren_child(_, {_, _} = state), do: state 171 172 defp find_issues([], acc, _issue_meta) do 173 acc 174 end 175 176 defp find_issues([{_, {line_no, _, _}, trigger} | t], acc, issue_meta) do 177 new_issue = issue_for(issue_meta, line_no, trigger) 178 179 acc = acc ++ [new_issue] 180 181 find_issues(t, acc, issue_meta) 182 end 183 184 defp issue_for(issue_meta, line_no, trigger) do 185 format_issue( 186 issue_meta, 187 message: "The condition of `#{trigger}` should not be wrapped in parentheses.", 188 trigger: trigger, 189 line_no: line_no 190 ) 191 end 192 end