nesting.ex (3681B)
1 defmodule Credo.Check.Refactor.Nesting do 2 use Credo.Check, 3 param_defaults: [max_nesting: 2], 4 explanations: [ 5 check: """ 6 Code should not be nested more than once inside a function. 7 8 defmodule CredoSampleModule do 9 def some_function(parameter1, parameter2) do 10 Enum.reduce(var1, list, fn({_hash, nodes}, list) -> 11 filenames = nodes |> Enum.map(&(&1.filename)) 12 13 Enum.reduce(list, [], fn(item, acc) -> 14 if item.filename do 15 item # <-- this is nested 3 levels deep 16 end 17 acc ++ [item] 18 end) 19 end) 20 end 21 end 22 23 At this point it might be a good idea to refactor the code to separate the 24 different loops and conditions. 25 """, 26 params: [ 27 max_nesting: "The maximum number of levels code should be nested." 28 ] 29 ] 30 31 @def_ops [:def, :defp, :defmacro] 32 @nest_ops [:if, :unless, :case, :cond, :fn] 33 34 @doc false 35 @impl true 36 def run(%SourceFile{} = source_file, params) do 37 issue_meta = IssueMeta.for(source_file, params) 38 max_nesting = Params.get(params, :max_nesting, __MODULE__) 39 40 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta, max_nesting)) 41 end 42 43 # TODO: consider for experimental check front-loader (ast) 44 for op <- @def_ops do 45 defp traverse( 46 {unquote(op) = op, meta, arguments} = ast, 47 issues, 48 issue_meta, 49 max_nesting 50 ) 51 when is_list(arguments) do 52 arguments 53 |> find_depth([], meta[:line], op) 54 |> handle_depth(ast, issue_meta, issues, max_nesting) 55 end 56 end 57 58 defp traverse(ast, issues, _issue_meta, _max_nesting) do 59 {ast, issues} 60 end 61 62 defp handle_depth(nil, ast, _issue_meta, issues, _max_nesting) do 63 {ast, issues} 64 end 65 66 defp handle_depth( 67 {depth, line_no, trigger}, 68 ast, 69 issue_meta, 70 issues, 71 max_nesting 72 ) do 73 if depth > max_nesting do 74 { 75 ast, 76 issues ++ [issue_for(issue_meta, line_no, trigger, max_nesting, depth)] 77 } 78 else 79 {ast, issues} 80 end 81 end 82 83 # Searches for the depth level and returns a tuple `{depth, line_no, trigger}` 84 # where the greatest depth was reached. 85 defp find_depth(arguments, nest_list, line_no, trigger) 86 when is_list(arguments) do 87 arguments 88 |> Credo.Code.Block.do_block_for!() 89 |> List.wrap() 90 |> Enum.map(&find_depth(&1, nest_list, line_no, trigger)) 91 |> Enum.sort() 92 |> List.last() 93 end 94 95 for op <- @nest_ops do 96 defp find_depth( 97 {unquote(op) = op, meta, arguments}, 98 nest_list, 99 _line_no, 100 _trigger 101 ) 102 when is_list(arguments) do 103 arguments 104 |> Enum.map(&find_depth(&1, nest_list ++ [op], meta[:line], op)) 105 |> Enum.sort() 106 |> List.last() 107 end 108 end 109 110 defp find_depth({atom, _meta, arguments}, nest_list, line_no, trigger) 111 when (is_atom(atom) or is_tuple(atom)) and is_list(arguments) do 112 arguments 113 |> Enum.map(&find_depth(&1, nest_list, line_no, trigger)) 114 |> Enum.sort() 115 |> List.last() 116 end 117 118 defp find_depth(_, nest_list, line_no, trigger) do 119 {Enum.count(nest_list), line_no, trigger} 120 end 121 122 defp issue_for(issue_meta, line_no, trigger, max_value, actual_value) do 123 format_issue( 124 issue_meta, 125 message: 126 "Function body is nested too deep (max depth is #{max_value}, was #{actual_value}).", 127 line_no: line_no, 128 trigger: trigger, 129 severity: Severity.compute(actual_value, max_value) 130 ) 131 end 132 end