default.ex (7607B)
1 defmodule Credo.CLI.Command.Suggest.Output.Default do 2 @moduledoc false 3 4 alias Credo.CLI.Filename 5 alias Credo.CLI.Output 6 alias Credo.CLI.Output.Summary 7 alias Credo.CLI.Output.UI 8 alias Credo.CLI.Sorter 9 alias Credo.Execution 10 alias Credo.Issue 11 alias Credo.SourceFile 12 13 @category_starting_order [:design, :readability, :refactor] 14 @category_ending_order [:warning, :consistency, :custom, :unknown] 15 @category_colors [ 16 design: :olive, 17 readability: :blue, 18 refactor: :yellow, 19 warning: :red, 20 consistency: :cyan 21 ] 22 @category_titles [ 23 design: "Software Design", 24 readability: "Code Readability", 25 refactor: "Refactoring opportunities", 26 warning: "Warnings - please take a look", 27 consistency: "Consistency" 28 ] 29 @many_source_files 60 30 @per_category 5 31 @indent 8 32 33 @doc "Called before the analysis is run." 34 def print_before_info(source_files, exec) do 35 case Enum.count(source_files) do 36 0 -> 37 UI.puts("No files found!") 38 39 1 -> 40 UI.puts("Checking 1 source file ...") 41 42 count -> 43 UI.puts("Checking #{count} source files#{checking_suffix(count)} ...") 44 end 45 46 Output.print_skipped_checks(exec) 47 end 48 49 defp checking_suffix(count) when count > @many_source_files do 50 " (this might take a while)" 51 end 52 53 defp checking_suffix(_), do: "" 54 55 @doc "Called after the analysis has run." 56 def print_after_info(source_files, exec, time_load, time_run) do 57 term_width = Output.term_columns() 58 59 issues = Execution.get_issues(exec) 60 61 categories = 62 issues 63 |> Enum.map(& &1.category) 64 |> Enum.uniq() 65 66 issue_map = 67 Enum.into(categories, %{}, fn category -> 68 {category, issues |> Enum.filter(&(&1.category == category))} 69 end) 70 71 source_file_map = Enum.into(source_files, %{}, &{&1.filename, &1}) 72 73 categories 74 |> Sorter.ensure(@category_starting_order, @category_ending_order) 75 |> Enum.each(fn category -> 76 print_issues_for_category( 77 category, 78 issue_map[category], 79 source_file_map, 80 exec, 81 term_width 82 ) 83 end) 84 85 source_files 86 |> Summary.print(exec, time_load, time_run) 87 end 88 89 defp print_issues_for_category( 90 _category, 91 nil, 92 _source_file_map, 93 _exec, 94 _term_width 95 ) do 96 nil 97 end 98 99 defp print_issues_for_category( 100 category, 101 issues, 102 source_file_map, 103 exec, 104 term_width 105 ) do 106 color = @category_colors[category] || :magenta 107 title = @category_titles[category] || "Category: #{category}" 108 109 UI.puts() 110 111 [ 112 :bright, 113 "#{color}_background" |> String.to_atom(), 114 color, 115 " ", 116 Output.foreground_color(color), 117 :normal, 118 " #{title}" |> String.pad_trailing(term_width - 1) 119 ] 120 |> UI.puts() 121 122 color 123 |> UI.edge() 124 |> UI.puts() 125 126 print_issues(issues, source_file_map, exec, term_width) 127 128 if Enum.count(issues) > per_category(exec) do 129 not_shown = Enum.count(issues) - per_category(exec) 130 131 [ 132 UI.edge(color), 133 :faint, 134 " ... (#{not_shown} more, use `--all` to show them)" 135 ] 136 |> UI.puts() 137 end 138 end 139 140 defp print_issues(issues, source_file_map, exec, term_width) do 141 count = per_category(exec) 142 143 issues 144 |> Enum.sort_by(fn issue -> 145 {issue.priority, issue.severity, issue.filename, issue.line_no} 146 end) 147 |> Enum.reverse() 148 |> Enum.take(count) 149 |> do_print_issues(source_file_map, exec, term_width) 150 end 151 152 defp per_category(%Execution{all: true}), do: 1_000_000 153 defp per_category(%Execution{all: false}), do: @per_category 154 155 defp do_print_issues( 156 issues, 157 source_file_map, 158 %Execution{format: _} = exec, 159 term_width 160 ) do 161 Enum.each(issues, fn %Issue{filename: filename} = issue -> 162 source_file = source_file_map[filename] 163 164 do_print_issue(issue, source_file, exec, term_width) 165 end) 166 end 167 168 defp do_print_issue( 169 %Issue{ 170 check: check, 171 message: message, 172 filename: filename, 173 priority: priority 174 } = issue, 175 source_file, 176 %Execution{format: _, verbose: verbose} = exec, 177 term_width 178 ) do 179 outer_color = Output.check_color(issue) 180 inner_color = Output.issue_color(issue) 181 message_color = outer_color 182 filename_color = :default_color 183 184 tag_style = 185 if outer_color == inner_color do 186 :faint 187 else 188 :bright 189 end 190 191 message = 192 if verbose do 193 message <> " [" <> inspect(check) <> "]" 194 else 195 message 196 end 197 198 message 199 |> UI.wrap_at(term_width - @indent) 200 |> print_issue_message( 201 check, 202 outer_color, 203 message_color, 204 tag_style, 205 priority 206 ) 207 208 [ 209 UI.edge(outer_color, @indent), 210 filename_color, 211 :faint, 212 filename |> to_string, 213 :default_color, 214 :faint, 215 Filename.pos_suffix(issue.line_no, issue.column), 216 :conceal, 217 " #", 218 :reset, 219 :faint, 220 "(#{issue.scope})" 221 ] 222 |> UI.puts() 223 224 if exec.verbose do 225 print_issue_line(issue, source_file, inner_color, outer_color, term_width) 226 227 UI.puts_edge([outer_color, :faint]) 228 end 229 end 230 231 defp print_issue_message( 232 [first_line | other_lines], 233 check, 234 outer_color, 235 message_color, 236 tag_style, 237 priority 238 ) do 239 [ 240 UI.edge(outer_color), 241 outer_color, 242 tag_style, 243 Output.check_tag(check.category), 244 " ", 245 priority |> Output.priority_arrow(), 246 :normal, 247 message_color, 248 " ", 249 first_line 250 ] 251 |> UI.puts() 252 253 other_lines 254 |> Enum.each(&print_issue_message(&1, outer_color, message_color)) 255 end 256 257 defp print_issue_message("", _outer_color, _message_color) do 258 end 259 260 defp print_issue_message(message, outer_color, message_color) do 261 [ 262 UI.edge(outer_color), 263 outer_color, 264 String.duplicate(" ", @indent - 3), 265 :normal, 266 message_color, 267 " ", 268 message 269 ] 270 |> UI.puts() 271 end 272 273 defp print_issue_line( 274 %Issue{line_no: nil}, 275 _source_file, 276 _inner_color, 277 _outer_color, 278 _term_width 279 ) do 280 nil 281 end 282 283 defp print_issue_line( 284 %Issue{} = issue, 285 source_file, 286 inner_color, 287 outer_color, 288 term_width 289 ) do 290 raw_line = SourceFile.line_at(source_file, issue.line_no) 291 line = String.trim(raw_line) 292 293 [outer_color, :faint] 294 |> UI.edge() 295 |> UI.puts() 296 297 [ 298 UI.edge([outer_color, :faint]), 299 :cyan, 300 :faint, 301 String.duplicate(" ", @indent - 2), 302 UI.truncate(line, term_width - @indent) 303 ] 304 |> UI.puts() 305 306 print_issue_trigger_marker(issue, raw_line, inner_color, outer_color) 307 end 308 309 defp print_issue_trigger_marker( 310 %Issue{column: nil}, 311 _line, 312 _inner_color, 313 _outer_color 314 ) do 315 nil 316 end 317 318 defp print_issue_trigger_marker( 319 %Issue{} = issue, 320 line, 321 inner_color, 322 outer_color 323 ) do 324 offset = String.length(line) - String.length(String.trim(line)) 325 326 # column is one-based 327 x = max(issue.column - offset - 1, 0) 328 329 w = 330 case issue.trigger do 331 nil -> 1 332 atom -> atom |> to_string |> String.length() 333 end 334 335 [ 336 UI.edge([outer_color, :faint], @indent), 337 inner_color, 338 String.duplicate(" ", x), 339 :faint, 340 String.duplicate("^", w) 341 ] 342 |> UI.puts() 343 end 344 end