default.ex (10670B)
1 defmodule Credo.CLI.Command.Diff.Output.Default do 2 @moduledoc false 3 4 alias Credo.CLI.Command.Diff.DiffCommand 5 alias Credo.CLI.Command.Diff.DiffSummary 6 alias Credo.CLI.Filename 7 alias Credo.CLI.Output 8 alias Credo.CLI.Output.UI 9 alias Credo.CLI.Sorter 10 alias Credo.Execution 11 alias Credo.Issue 12 alias Credo.SourceFile 13 14 @category_starting_order [:design, :readability, :refactor] 15 @category_ending_order [:warning, :consistency, :custom, :unknown] 16 @category_colors [ 17 design: :olive, 18 readability: :blue, 19 refactor: :yellow, 20 warning: :red, 21 consistency: :cyan 22 ] 23 @category_titles [ 24 design: "Software Design", 25 readability: "Code Readability", 26 refactor: "Refactoring opportunities", 27 warning: "Warnings - please take a look", 28 consistency: "Consistency" 29 ] 30 @many_source_files 60 31 @per_category 5 32 @indent 8 33 34 @doc "Called before the analysis is run." 35 def print_before_info(source_files, exec) do 36 {_, git_ref_or_range} = DiffCommand.previous_ref(exec) 37 38 case Enum.count(source_files) do 39 0 -> 40 UI.puts("No files found!") 41 42 1 -> 43 UI.puts([ 44 :faint, 45 "Diffing 1 source file in working dir with ", 46 :cyan, 47 git_ref_or_range, 48 :reset, 49 :faint, 50 " ..." 51 ]) 52 53 count -> 54 UI.puts([ 55 :faint, 56 "Diffing #{count} source files in working dir with ", 57 :cyan, 58 git_ref_or_range, 59 :reset, 60 :faint, 61 "#{checking_suffix(count)} ..." 62 ]) 63 end 64 65 Output.print_skipped_checks(exec) 66 end 67 68 defp checking_suffix(count) when count > @many_source_files do 69 " (this might take a while)" 70 end 71 72 defp checking_suffix(_), do: "" 73 74 @doc "Called after the analysis has run." 75 def print_after_info(source_files, exec, time_load, time_run) do 76 term_width = Output.term_columns() 77 filtered_diff_markers = filtered_diff_markers(exec) 78 79 issues_to_display = 80 exec 81 |> Execution.get_issues() 82 |> Enum.filter(&Enum.member?(filtered_diff_markers, &1.diff_marker)) 83 84 categories = 85 issues_to_display 86 |> Enum.map(& &1.category) 87 |> Enum.uniq() 88 89 issue_map = 90 Enum.into(categories, %{}, fn category -> 91 {category, Enum.filter(issues_to_display, &(&1.category == category))} 92 end) 93 94 source_file_map = Enum.into(source_files, %{}, &{&1.filename, &1}) 95 96 categories 97 |> Sorter.ensure(@category_starting_order, @category_ending_order) 98 |> Enum.each(fn category -> 99 print_issues_for_category( 100 category, 101 issue_map[category], 102 source_file_map, 103 exec, 104 term_width 105 ) 106 end) 107 108 DiffSummary.print(source_files, exec, time_load, time_run) 109 end 110 111 defp print_issues_for_category( 112 _category, 113 nil, 114 _source_file_map, 115 _exec, 116 _term_width 117 ) do 118 nil 119 end 120 121 defp print_issues_for_category( 122 category, 123 issues, 124 source_file_map, 125 exec, 126 term_width 127 ) do 128 color = @category_colors[category] || :magenta 129 title = @category_titles[category] || "Category: #{category}" 130 131 UI.puts() 132 133 [ 134 diff_marker(1, color), 135 :bright, 136 "#{color}_background" |> String.to_atom(), 137 color, 138 " ", 139 Output.foreground_color(color), 140 :normal, 141 " #{title}" |> String.pad_trailing(term_width - 3) 142 ] 143 |> UI.puts() 144 145 UI.puts([ 146 diff_marker(2, color), 147 UI.edge(color) 148 ]) 149 150 print_issues(issues, source_file_map, exec, term_width) 151 152 if Enum.count(issues) > per_category(exec) do 153 not_shown = Enum.count(issues) - per_category(exec) 154 155 [ 156 diff_marker(), 157 UI.edge(color), 158 :faint, 159 " ... (#{not_shown} other new issues, use `--all` to show them)" 160 ] 161 |> UI.puts() 162 end 163 end 164 165 defp print_issues(issues, source_file_map, exec, term_width) do 166 count = per_category(exec) 167 sort_weight = %{fixed: 0, old: 1, new: 2} 168 169 issues 170 |> Enum.sort_by(fn issue -> 171 {sort_weight[issue.diff_marker], issue.priority, issue.severity, issue.filename, 172 issue.line_no} 173 end) 174 |> Enum.reverse() 175 |> Enum.take(count) 176 |> do_print_issues(source_file_map, exec, term_width) 177 end 178 179 defp per_category(%Execution{all: true}), do: 1_000_000 180 defp per_category(%Execution{all: false}), do: @per_category 181 182 defp do_print_issues( 183 issues, 184 source_file_map, 185 %Execution{format: _} = exec, 186 term_width 187 ) do 188 Enum.each(issues, fn %Issue{filename: filename} = issue -> 189 source_file = source_file_map[filename] 190 191 do_print_issue(issue, source_file, exec, term_width) 192 end) 193 end 194 195 defp do_print_issue( 196 %Issue{ 197 check: check, 198 message: message, 199 filename: filename, 200 priority: priority 201 } = issue, 202 source_file, 203 %Execution{format: _, verbose: verbose} = exec, 204 term_width 205 ) do 206 new_issue? = issue.diff_marker == :new 207 fixed_issue? = issue.diff_marker == :fixed 208 209 outer_color = 210 if new_issue? do 211 Output.check_color(issue) 212 else 213 [Output.check_color(issue), :faint] 214 end 215 216 inner_color = 217 if new_issue? do 218 Output.issue_color(issue) 219 else 220 [Output.issue_color(issue), :faint] 221 end 222 223 message_color = outer_color 224 filename_color = :default_color 225 226 tag_style = 227 if outer_color == inner_color do 228 :faint 229 else 230 :bright 231 end 232 233 message = 234 if verbose do 235 message <> " (" <> inspect(check) <> ")" 236 else 237 message 238 end 239 240 message 241 |> UI.wrap_at(term_width - @indent) 242 |> print_issue_message( 243 issue, 244 check, 245 outer_color, 246 message_color, 247 tag_style, 248 priority 249 ) 250 251 location = 252 if fixed_issue? do 253 given_ref = Execution.get_assign(exec, "credo.diff.given_ref") 254 previous_dirname = Execution.get_assign(exec, "credo.diff.previous_dirname") 255 256 case given_ref do 257 {:path, path} -> 258 relative_filename = String.replace(filename, previous_dirname, "") 259 260 "(dir:#{path}) #{relative_filename}" 261 262 _ -> 263 git_ref = Execution.get_assign(exec, "credo.diff.previous_git_ref") 264 265 relative_filename = 266 filename |> String.replace(previous_dirname, "") |> String.replace(~r/^[\/\\]/, "") 267 268 "(git:#{git_ref}) #{relative_filename}" 269 end 270 else 271 to_string(filename) 272 end 273 274 [ 275 diff_marker(issue.diff_marker), 276 UI.edge(outer_color, @indent), 277 filename_color, 278 :faint, 279 location, 280 :default_color, 281 :faint, 282 Filename.pos_suffix(issue.line_no, issue.column), 283 :conceal, 284 " #", 285 :reset, 286 :faint, 287 "(#{issue.scope})" 288 ] 289 |> UI.puts() 290 291 if exec.verbose && issue.diff_marker == :new do 292 print_issue_line(issue, source_file, inner_color, outer_color, term_width) 293 294 [ 295 diff_marker(issue.diff_marker), 296 UI.edge([ 297 outer_color, 298 :faint 299 ]) 300 ] 301 |> UI.puts() 302 end 303 end 304 305 defp print_issue_message( 306 [first_line | other_lines], 307 issue, 308 check, 309 outer_color, 310 message_color, 311 tag_style, 312 priority 313 ) do 314 [ 315 diff_marker(issue.diff_marker), 316 UI.edge(outer_color), 317 outer_color, 318 tag_style, 319 Output.check_tag(check.category), 320 " ", 321 priority |> Output.priority_arrow(), 322 :normal, 323 message_color, 324 " ", 325 first_line 326 ] 327 |> UI.puts() 328 329 other_lines 330 |> Enum.each(&print_issue_message(&1, issue, outer_color, message_color)) 331 end 332 333 defp print_issue_message( 334 "", 335 _issue, 336 _outer_color, 337 _message_color 338 ) do 339 end 340 341 defp print_issue_message( 342 message, 343 issue, 344 outer_color, 345 message_color 346 ) do 347 [ 348 diff_marker(issue.diff_marker), 349 UI.edge(outer_color), 350 outer_color, 351 String.duplicate(" ", @indent - 3), 352 :normal, 353 message_color, 354 " ", 355 message 356 ] 357 |> UI.puts() 358 end 359 360 defp print_issue_line( 361 %Issue{line_no: nil}, 362 _source_file, 363 _inner_color, 364 _outer_color, 365 _term_width 366 ) do 367 nil 368 end 369 370 defp print_issue_line( 371 %Issue{} = issue, 372 source_file, 373 inner_color, 374 outer_color, 375 term_width 376 ) do 377 raw_line = SourceFile.line_at(source_file, issue.line_no) 378 line = String.trim(raw_line) 379 380 [diff_marker(issue.diff_marker), UI.edge([outer_color, :faint])] 381 |> UI.puts() 382 383 [ 384 diff_marker(issue.diff_marker), 385 UI.edge([outer_color, :faint]), 386 :cyan, 387 :faint, 388 String.duplicate(" ", @indent - 2), 389 UI.truncate(line, term_width - @indent) 390 ] 391 |> UI.puts() 392 393 print_issue_trigger_marker(issue, raw_line, inner_color, outer_color) 394 end 395 396 defp print_issue_trigger_marker( 397 %Issue{column: nil}, 398 _line, 399 _inner_color, 400 _outer_color 401 ) do 402 nil 403 end 404 405 defp print_issue_trigger_marker( 406 %Issue{} = issue, 407 line, 408 inner_color, 409 outer_color 410 ) do 411 offset = String.length(line) - String.length(String.trim(line)) 412 413 # column is one-based 414 x = max(issue.column - offset - 1, 0) 415 416 w = 417 case issue.trigger do 418 nil -> 1 419 atom -> atom |> to_string |> String.length() 420 end 421 422 [ 423 diff_marker(issue.diff_marker), 424 UI.edge([outer_color, :faint], @indent), 425 inner_color, 426 String.duplicate(" ", x), 427 :faint, 428 String.duplicate("^", w) 429 ] 430 |> UI.puts() 431 end 432 433 defp filtered_diff_markers(exec) do 434 filtered_diff_markers = [:new] 435 436 filtered_diff_markers = 437 if exec.cli_options.switches[:show_kept] do 438 filtered_diff_markers ++ [:old] 439 else 440 filtered_diff_markers 441 end 442 443 if exec.cli_options.switches[:show_fixed] do 444 filtered_diff_markers ++ [:fixed] 445 else 446 filtered_diff_markers 447 end 448 end 449 450 defp diff_marker do 451 [:faint, " ", :reset, ""] 452 end 453 454 defp diff_marker(1, color) do 455 [color, :faint, " ", :reset, ""] 456 end 457 458 defp diff_marker(2, color) do 459 [color, :faint, " ", :reset, ""] 460 end 461 462 defp diff_marker(:new) do 463 [:green, :bright, "+ ", :reset, ""] 464 end 465 466 defp diff_marker(:old) do 467 [:faint, "~ ", :reset, ""] 468 end 469 470 defp diff_marker(:fixed) do 471 [:faint, "✔ ", :reset, ""] 472 end 473 end