pipe_chain_start.ex (7608B)
1 defmodule Credo.Check.Refactor.PipeChainStart do 2 use Credo.Check, 3 tags: [:controversial], 4 param_defaults: [ 5 excluded_argument_types: [], 6 excluded_functions: [] 7 ], 8 explanations: [ 9 check: """ 10 Pipes (`|>`) can become more readable by starting with a "raw" value. 11 12 So while this is easily comprehendable: 13 14 list 15 |> Enum.take(5) 16 |> Enum.shuffle 17 |> pick_winner() 18 19 This might be harder to read: 20 21 Enum.take(list, 5) 22 |> Enum.shuffle 23 |> pick_winner() 24 25 As always: This is just a suggestion. Check the configuration options for 26 tweaking or disabling this check. 27 """, 28 params: [ 29 excluded_functions: "All functions listed will be ignored.", 30 excluded_argument_types: "All pipes with argument types listed will be ignored." 31 ] 32 ] 33 34 @elixir_custom_operators [ 35 :<-, 36 :|||, 37 :&&&, 38 :<<<, 39 :>>>, 40 :<<~, 41 :~>>, 42 :<~, 43 :~>, 44 :<~>, 45 :<|>, 46 :^^^, 47 :~~~, 48 :"..//" 49 ] 50 51 @doc false 52 @impl true 53 def run(%SourceFile{} = source_file, params) do 54 issue_meta = IssueMeta.for(source_file, params) 55 56 excluded_functions = Params.get(params, :excluded_functions, __MODULE__) 57 58 excluded_argument_types = Params.get(params, :excluded_argument_types, __MODULE__) 59 60 Credo.Code.prewalk( 61 source_file, 62 &traverse(&1, &2, issue_meta, excluded_functions, excluded_argument_types) 63 ) 64 end 65 66 # TODO: consider for experimental check front-loader (ast) 67 defp traverse( 68 {:|>, _, [{:|>, _, _} | _]} = ast, 69 issues, 70 _issue_meta, 71 _excluded_functions, 72 _excluded_argument_types 73 ) do 74 {ast, issues} 75 end 76 77 defp traverse( 78 {:|>, meta, [lhs | _rhs]} = ast, 79 issues, 80 issue_meta, 81 excluded_functions, 82 excluded_argument_types 83 ) do 84 if valid_chain_start?(lhs, excluded_functions, excluded_argument_types) do 85 {ast, issues} 86 else 87 {ast, issues ++ [issue_for(issue_meta, meta[:line], "TODO")]} 88 end 89 end 90 91 defp traverse( 92 ast, 93 issues, 94 _issue_meta, 95 _excluded_functions, 96 _excluded_argument_types 97 ) do 98 {ast, issues} 99 end 100 101 defp valid_chain_start?( 102 {:__block__, _, [single_ast_node]}, 103 excluded_functions, 104 excluded_argument_types 105 ) do 106 valid_chain_start?( 107 single_ast_node, 108 excluded_functions, 109 excluded_argument_types 110 ) 111 end 112 113 for atom <- [ 114 :%, 115 :%{}, 116 :.., 117 :<<>>, 118 :@, 119 :__aliases__, 120 :unquote, 121 :{}, 122 :&, 123 :<>, 124 :++, 125 :--, 126 :&&, 127 :||, 128 :+, 129 :-, 130 :*, 131 :/, 132 :>, 133 :>=, 134 :<, 135 :<=, 136 :==, 137 :for, 138 :with, 139 :not, 140 :and, 141 :or 142 ] do 143 defp valid_chain_start?( 144 {unquote(atom), _meta, _arguments}, 145 _excluded_functions, 146 _excluded_argument_types 147 ) do 148 true 149 end 150 end 151 152 for operator <- @elixir_custom_operators do 153 defp valid_chain_start?( 154 {unquote(operator), _meta, _arguments}, 155 _excluded_functions, 156 _excluded_argument_types 157 ) do 158 true 159 end 160 end 161 162 # anonymous function 163 defp valid_chain_start?( 164 {:fn, _, [{:->, _, [_args, _body]}]}, 165 _excluded_functions, 166 _excluded_argument_types 167 ) do 168 true 169 end 170 171 # function_call() 172 defp valid_chain_start?( 173 {atom, _, []}, 174 _excluded_functions, 175 _excluded_argument_types 176 ) 177 when is_atom(atom) do 178 true 179 end 180 181 # function_call(with, args) and sigils 182 defp valid_chain_start?( 183 {atom, _, arguments} = ast, 184 excluded_functions, 185 excluded_argument_types 186 ) 187 when is_atom(atom) and is_list(arguments) do 188 sigil?(atom) || 189 valid_chain_start_function_call?( 190 ast, 191 excluded_functions, 192 excluded_argument_types 193 ) 194 end 195 196 # map[:access] 197 defp valid_chain_start?( 198 {{:., _, [Access, :get]}, _, _}, 199 _excluded_functions, 200 _excluded_argument_types 201 ) do 202 true 203 end 204 205 # Module.function_call() 206 defp valid_chain_start?( 207 {{:., _, _}, _, []}, 208 _excluded_functions, 209 _excluded_argument_types 210 ), 211 do: true 212 213 # Elixir <= 1.8.0 214 # '__#{val}__' are compiled to String.to_charlist("__#{val}__") 215 # we want to consider these charlists a valid pipe chain start 216 defp valid_chain_start?( 217 {{:., _, [String, :to_charlist]}, _, [{:<<>>, _, _}]}, 218 _excluded_functions, 219 _excluded_argument_types 220 ), 221 do: true 222 223 # Elixir >= 1.8.0 224 # '__#{val}__' are compiled to String.to_charlist("__#{val}__") 225 # we want to consider these charlists a valid pipe chain start 226 defp valid_chain_start?( 227 {{:., _, [List, :to_charlist]}, _, [[_ | _]]}, 228 _excluded_functions, 229 _excluded_argument_types 230 ), 231 do: true 232 233 # Module.function_call(with, parameters) 234 defp valid_chain_start?( 235 {{:., _, _}, _, _} = ast, 236 excluded_functions, 237 excluded_argument_types 238 ) do 239 valid_chain_start_function_call?( 240 ast, 241 excluded_functions, 242 excluded_argument_types 243 ) 244 end 245 246 defp valid_chain_start?(_, _excluded_functions, _excluded_argument_types), do: true 247 248 defp valid_chain_start_function_call?( 249 {_atom, _, arguments} = ast, 250 excluded_functions, 251 excluded_argument_types 252 ) do 253 function_name = to_function_call_name(ast) 254 255 found_argument_types = 256 case arguments do 257 [nil | _] -> [:atom] 258 x -> x |> List.first() |> argument_type() 259 end 260 261 Enum.member?(excluded_functions, function_name) || 262 Enum.any?( 263 found_argument_types, 264 &Enum.member?(excluded_argument_types, &1) 265 ) 266 end 267 268 defp sigil?(atom) do 269 atom 270 |> to_string 271 |> String.match?(~r/^sigil_[a-zA-Z]$/) 272 end 273 274 defp to_function_call_name({_, _, _} = ast) do 275 {ast, [], []} 276 |> Macro.to_string() 277 |> String.replace(~r/\.?\(.*\)$/s, "") 278 end 279 280 @alphabet_wo_r ~w(a b c d e f g h i j k l m n o p q s t u v w x y z) 281 @all_sigil_chars Enum.flat_map(@alphabet_wo_r, &[&1, String.upcase(&1)]) 282 @matchable_sigils Enum.map(@all_sigil_chars, &:"sigil_#{&1}") 283 284 for sigil_atom <- @matchable_sigils do 285 defp argument_type({unquote(sigil_atom), _, _}) do 286 [unquote(sigil_atom)] 287 end 288 end 289 290 defp argument_type({:sigil_r, _, _}), do: [:sigil_r, :regex] 291 defp argument_type({:sigil_R, _, _}), do: [:sigil_R, :regex] 292 293 defp argument_type({:fn, _, _}), do: [:fn] 294 defp argument_type({:%{}, _, _}), do: [:map] 295 defp argument_type({:{}, _, _}), do: [:tuple] 296 defp argument_type(nil), do: [] 297 298 defp argument_type(v) when is_atom(v), do: [:atom] 299 defp argument_type(v) when is_binary(v), do: [:binary] 300 defp argument_type(v) when is_bitstring(v), do: [:bitstring] 301 defp argument_type(v) when is_boolean(v), do: [:boolean] 302 303 defp argument_type(v) when is_list(v) do 304 if Keyword.keyword?(v) do 305 [:keyword, :list] 306 else 307 [:list] 308 end 309 end 310 311 defp argument_type(v) when is_number(v), do: [:number] 312 313 defp argument_type(v), do: [:credo_type_error, v] 314 315 defp issue_for(issue_meta, line_no, trigger) do 316 format_issue( 317 issue_meta, 318 message: "Pipe chain should start with a raw value.", 319 trigger: trigger, 320 line_no: line_no 321 ) 322 end 323 end