long_quote_blocks.ex (4126B)
1 defmodule Credo.Check.Refactor.LongQuoteBlocks do 2 use Credo.Check, 3 base_priority: :high, 4 param_defaults: [max_line_count: 150, ignore_comments: false], 5 explanations: [ 6 check: """ 7 Long `quote` blocks are generally an indication that too much is done inside 8 them. 9 10 Let's look at why this is problematic: 11 12 defmodule MetaCommand do 13 def __using__(opts \\\\ []) do 14 modes = opts[:modes] 15 command_name = opts[:command_name] 16 17 quote do 18 def run(filename) do 19 contents = 20 if File.exists?(filename) do 21 {:ok, file} = File.open(filename, unquote(modes)) 22 {:ok, contents} = IO.read(file, :line) 23 File.close(file) 24 contents 25 else 26 "" 27 end 28 29 case contents do 30 "" -> 31 # ... 32 unquote(command_name) <> rest -> 33 # ... 34 end 35 end 36 37 # ... 38 end 39 end 40 end 41 42 A cleaner solution would be to call "regular" functions outside the 43 `quote` block to perform the actual work. 44 45 defmodule MyMetaCommand do 46 def __using__(opts \\\\ []) do 47 modes = opts[:modes] 48 command_name = opts[:command_name] 49 50 quote do 51 def run(filename) do 52 MyMetaCommand.run_on_file(filename, unquote(modes), unquote(command_name)) 53 end 54 55 # ... 56 end 57 end 58 59 def run_on_file(filename, modes, command_name) do 60 contents = 61 # actual implementation 62 end 63 end 64 65 This way it is easier to reason about what is actually happening. And to debug 66 it. 67 """, 68 params: [ 69 max_line_count: "The maximum number of lines a quote block should be allowed to have.", 70 ignore_comments: "Ignores comments when counting the lines of a `quote` block." 71 ] 72 ] 73 74 alias Credo.IssueMeta 75 76 @doc false 77 @impl true 78 def run(%SourceFile{} = source_file, params) do 79 issue_meta = IssueMeta.for(source_file, params) 80 max_line_count = Params.get(params, :max_line_count, __MODULE__) 81 ignore_comments = Params.get(params, :ignore_comments, __MODULE__) 82 83 Credo.Code.prewalk( 84 source_file, 85 &traverse(&1, &2, issue_meta, max_line_count, ignore_comments) 86 ) 87 end 88 89 # TODO: consider for experimental check front-loader (ast) 90 defp traverse( 91 {:quote, meta, arguments} = ast, 92 issues, 93 issue_meta, 94 max_line_count, 95 ignore_comments 96 ) do 97 max_line_no = Credo.Code.prewalk(arguments, &find_max_line_no(&1, &2), 0) 98 line_count = max_line_no - meta[:line] 99 100 issue = 101 if line_count > max_line_count do 102 source_file = IssueMeta.source_file(issue_meta) 103 104 lines = 105 source_file 106 |> Credo.Code.to_lines() 107 |> Enum.slice(meta[:line] - 1, line_count) 108 109 lines = 110 if ignore_comments do 111 Enum.reject(lines, fn {_line_no, line} -> 112 Regex.run(~r/^\s*#/, line) 113 end) 114 else 115 lines 116 end 117 118 if Enum.count(lines) > max_line_count do 119 issue_for(issue_meta, meta[:line]) 120 end 121 end 122 123 {ast, issues ++ List.wrap(issue)} 124 end 125 126 defp traverse(ast, issues, _issue_meta, _max_line_count, _ignore_comments) do 127 {ast, issues} 128 end 129 130 defp find_max_line_no({_, meta, _} = ast, max_line_no) do 131 line_no = meta[:line] || 0 132 133 if line_no > max_line_no do 134 {ast, line_no} 135 else 136 {ast, max_line_no} 137 end 138 end 139 140 defp find_max_line_no(ast, max_line_no) do 141 {ast, max_line_no} 142 end 143 144 defp issue_for(issue_meta, line_no) do 145 format_issue( 146 issue_meta, 147 message: "Avoid long quote blocks.", 148 trigger: "quote", 149 line_no: line_no 150 ) 151 end 152 end