nested_function_calls.ex (3384B)
1 defmodule Credo.Check.Readability.NestedFunctionCalls do 2 use Credo.Check, 3 tags: [:controversial], 4 param_defaults: [min_pipeline_length: 2], 5 explanations: [ 6 check: """ 7 A function call should not be nested inside another function call. 8 9 So while this is fine: 10 11 Enum.shuffle([1,2,3]) 12 13 The code in this example ... 14 15 Enum.shuffle(Enum.uniq([1,2,3,3])) 16 17 ... should be refactored to look like this: 18 19 [1,2,3,3] 20 |> Enum.uniq() 21 |> Enum.shuffle() 22 23 Nested function calls make the code harder to read. Instead, break the 24 function calls out into a pipeline. 25 26 Like all `Readability` issues, this one is not a technical concern. 27 But you can improve the odds of others reading and liking your code by making 28 it easier to follow. 29 """, 30 params: [ 31 min_pipeline_length: "Set a minimum pipeline length" 32 ] 33 ] 34 35 alias Credo.Code.Name 36 37 @doc false 38 @impl true 39 def run(%SourceFile{} = source_file, params) do 40 issue_meta = IssueMeta.for(source_file, params) 41 42 min_pipeline_length = Params.get(params, :min_pipeline_length, __MODULE__) 43 44 {_continue, issues} = 45 Credo.Code.prewalk( 46 source_file, 47 &traverse(&1, &2, issue_meta, min_pipeline_length), 48 {true, []} 49 ) 50 51 issues 52 end 53 54 # A call with no arguments 55 defp traverse({{:., _loc, _call}, _meta, []} = ast, {_, issues}, _, min_pipeline_length) do 56 {ast, {min_pipeline_length, issues}} 57 end 58 59 # A call with arguments 60 defp traverse( 61 {{:., _loc, call}, meta, args} = ast, 62 {_, issues}, 63 issue_meta, 64 min_pipeline_length 65 ) do 66 if valid_chain_start?(ast) do 67 {ast, {min_pipeline_length, issues}} 68 else 69 case length_as_pipeline(args) + 1 do 70 potential_pipeline_length when potential_pipeline_length >= min_pipeline_length -> 71 {ast, 72 {min_pipeline_length, issues ++ [issue_for(issue_meta, meta[:line], Name.full(call))]}} 73 74 _ -> 75 {ast, {min_pipeline_length, issues}} 76 end 77 end 78 end 79 80 # Another expression 81 defp traverse(ast, {_, issues}, _issue_meta, min_pipeline_length) do 82 {ast, {min_pipeline_length, issues}} 83 end 84 85 # Call with no arguments 86 defp length_as_pipeline([{{:., _loc, _call}, _meta, []} | _]) do 87 0 88 end 89 90 # Call with function call for first argument 91 defp length_as_pipeline([{{:., _loc, _call}, _meta, args} = call_ast | _]) do 92 if valid_chain_start?(call_ast) do 93 0 94 else 95 1 + length_as_pipeline(args) 96 end 97 end 98 99 # Call where the first argument isn't another function call 100 defp length_as_pipeline(_args) do 101 0 102 end 103 104 defp issue_for(issue_meta, line_no, trigger) do 105 format_issue( 106 issue_meta, 107 message: "Use a pipeline when there are nested function calls", 108 trigger: trigger, 109 line_no: line_no 110 ) 111 end 112 113 # Taken from the Credo.Check.Refactor.PipeChainStart module, with modifications 114 # map[:access] 115 defp valid_chain_start?({{:., _, [Access, :get]}, _, _}), do: true 116 117 # Module.function_call() 118 defp valid_chain_start?({{:., _, _}, _, []}), do: true 119 120 # Kernel.to_string is invoked for string interpolation e.g. "string #{variable}" 121 defp valid_chain_start?({{:., _, [Kernel, :to_string]}, _, _}), do: true 122 123 defp valid_chain_start?(_), do: false 124 end