default.ex (12638B)
1 defmodule Credo.CLI.Command.Explain.Output.Default do 2 @moduledoc false 3 4 alias Credo.CLI.Output 5 alias Credo.CLI.Output.UI 6 alias Credo.Code.Scope 7 8 @indent 8 9 @params_min_indent 10 10 11 @doc "Called before the analysis is run." 12 def print_before_info(source_files, exec) do 13 UI.puts() 14 15 case Enum.count(source_files) do 16 0 -> UI.puts("No files found!") 17 1 -> UI.puts("Checking 1 source file ...") 18 count -> UI.puts("Checking #{count} source files ...") 19 end 20 21 Output.print_skipped_checks(exec) 22 end 23 24 @doc "Called after the analysis has run." 25 def print_after_info(explanations, exec, nil, nil) do 26 term_width = Output.term_columns() 27 28 print_explanations_for_check(explanations, exec, term_width) 29 end 30 31 def print_after_info(explanations, exec, line_no, column) do 32 term_width = Output.term_columns() 33 34 print_explanations_for_issue(explanations, exec, term_width, line_no, column) 35 end 36 37 # 38 # CHECK 39 # 40 41 defp print_explanations_for_check(explanations, _exec, term_width) do 42 Enum.each(explanations, &print_check(&1, term_width)) 43 end 44 45 defp print_check( 46 %{ 47 category: category, 48 check: check, 49 explanation_for_issue: explanation_for_issue, 50 priority: priority 51 }, 52 term_width 53 ) do 54 check_name = check |> to_string |> String.replace(~r/^Elixir\./, "") 55 color = Output.check_color(check.category) 56 57 UI.puts() 58 59 [ 60 :bright, 61 "#{color}_background" |> String.to_atom(), 62 color, 63 " ", 64 Output.foreground_color(color), 65 :normal, 66 " Check: #{check_name}" |> String.pad_trailing(term_width - 1) 67 ] 68 |> UI.puts() 69 70 UI.puts_edge(color) 71 72 outer_color = Output.check_color(category) 73 inner_color = Output.check_color(category) 74 75 tag_style = 76 if outer_color == inner_color do 77 :faint 78 else 79 :bright 80 end 81 82 [ 83 UI.edge(outer_color), 84 inner_color, 85 tag_style, 86 " ", 87 Output.check_tag(check.category), 88 :reset, 89 " Category: #{check.category} " 90 ] 91 |> UI.puts() 92 93 [ 94 UI.edge(outer_color), 95 inner_color, 96 tag_style, 97 " ", 98 priority |> Output.priority_arrow(), 99 :reset, 100 " Priority: #{Output.priority_name(priority)} " 101 ] 102 |> UI.puts() 103 104 UI.puts_edge(outer_color) 105 106 UI.puts_edge([outer_color, :faint], @indent) 107 108 print_check_explanation(explanation_for_issue, outer_color) 109 print_params_explanation(check, outer_color) 110 111 UI.puts_edge([outer_color, :faint]) 112 end 113 114 # 115 # ISSUE 116 # 117 118 defp print_explanations_for_issue( 119 [], 120 _exec, 121 _term_width, 122 _line_no, 123 _column 124 ) do 125 nil 126 end 127 128 defp print_explanations_for_issue( 129 explanations, 130 _exec, 131 term_width, 132 _line_no, 133 _column 134 ) do 135 first_explanation = explanations |> List.first() 136 scope_name = Scope.mod_name(first_explanation.scope) 137 color = Output.check_color(first_explanation.category) 138 139 UI.puts() 140 141 [ 142 :bright, 143 "#{color}_background" |> String.to_atom(), 144 color, 145 " ", 146 Output.foreground_color(color), 147 :normal, 148 " #{scope_name}" |> String.pad_trailing(term_width - 1) 149 ] 150 |> UI.puts() 151 152 UI.puts_edge(color) 153 154 Enum.each(explanations, &print_issue(&1, term_width)) 155 end 156 157 defp print_issue( 158 %{ 159 category: category, 160 check: check, 161 column: column, 162 explanation_for_issue: explanation_for_issue, 163 filename: filename, 164 trigger: trigger, 165 line_no: line_no, 166 message: message, 167 priority: priority, 168 related_code: related_code, 169 scope: scope 170 }, 171 term_width 172 ) do 173 pos = pos_string(line_no, column) 174 175 outer_color = Output.check_color(category) 176 inner_color = Output.check_color(category) 177 message_color = inner_color 178 filename_color = :default_color 179 180 tag_style = 181 if outer_color == inner_color do 182 :faint 183 else 184 :bright 185 end 186 187 [ 188 UI.edge(outer_color), 189 inner_color, 190 tag_style, 191 " ", 192 Output.check_tag(check.category), 193 :reset, 194 " Category: #{check.category} " 195 ] 196 |> UI.puts() 197 198 [ 199 UI.edge(outer_color), 200 inner_color, 201 tag_style, 202 " ", 203 priority |> Output.priority_arrow(), 204 :reset, 205 " Priority: #{Output.priority_name(priority)} " 206 ] 207 |> UI.puts() 208 209 UI.puts_edge(outer_color) 210 211 [ 212 UI.edge(outer_color), 213 inner_color, 214 tag_style, 215 " ", 216 :normal, 217 message_color, 218 " ", 219 message 220 ] 221 |> UI.puts() 222 223 [ 224 UI.edge(outer_color, @indent), 225 filename_color, 226 :faint, 227 to_string(filename), 228 :default_color, 229 :faint, 230 pos, 231 :faint, 232 " (#{scope})" 233 ] 234 |> UI.puts() 235 236 if line_no do 237 print_issue_line_no( 238 term_width, 239 line_no, 240 column, 241 trigger, 242 related_code, 243 outer_color, 244 inner_color 245 ) 246 end 247 248 UI.puts_edge([outer_color, :faint], @indent) 249 250 print_check_explanation(explanation_for_issue, outer_color) 251 print_params_explanation(check, outer_color) 252 253 UI.puts_edge([outer_color, :faint]) 254 end 255 256 def print_check_explanation(explanation_for_issue, outer_color) do 257 [ 258 UI.edge([outer_color, :faint]), 259 :reset, 260 :color239, 261 String.duplicate(" ", @indent - 5), 262 "__ WHY IT MATTERS" 263 ] 264 |> UI.puts() 265 266 UI.puts_edge([outer_color, :faint]) 267 268 (explanation_for_issue || "TODO: Insert explanation") 269 |> String.trim() 270 |> String.split("\n") 271 |> Enum.flat_map(&format_explanation(&1, outer_color)) 272 |> Enum.slice(0..-2) 273 |> UI.puts() 274 275 UI.puts_edge([outer_color, :faint]) 276 end 277 278 def format_explanation(line, outer_color) do 279 [ 280 UI.edge([outer_color, :faint], @indent), 281 :reset, 282 line |> format_explanation_text, 283 "\n" 284 ] 285 end 286 287 def format_explanation_text(" " <> line) do 288 [:yellow, :faint, " ", line] 289 end 290 291 def format_explanation_text(line) do 292 # TODO: format things in backticks in help texts 293 # case Regex.run(~r/(\`[a-zA-Z_\.]+\`)/, line) do 294 # v -> 295 # # IO.inspect(v) 296 [:reset, line] 297 # end 298 end 299 300 defp pos_string(nil, nil), do: "" 301 defp pos_string(line_no, nil), do: ":#{line_no}" 302 defp pos_string(line_no, column), do: ":#{line_no}:#{column}" 303 304 def print_params_explanation(nil, _), do: nil 305 306 def print_params_explanation(check, outer_color) do 307 check_name = check |> to_string |> String.replace(~r/^Elixir\./, "") 308 309 [ 310 UI.edge([outer_color, :faint]), 311 :reset, 312 :color239, 313 String.duplicate(" ", @indent - 5), 314 "__ CONFIGURATION OPTIONS" 315 ] 316 |> UI.puts() 317 318 UI.puts_edge([outer_color, :faint]) 319 320 print_params_explanation( 321 outer_color, 322 check_name, 323 check.explanations()[:params], 324 check.param_defaults() 325 ) 326 end 327 328 def print_params_explanation(outer_color, check_name, param_explanations, _defaults) 329 when param_explanations in [nil, []] do 330 [ 331 UI.edge([outer_color, :faint]), 332 :reset, 333 String.duplicate(" ", @indent - 2), 334 "You can disable this check by using this tuple" 335 ] 336 |> UI.puts() 337 338 UI.puts_edge([outer_color, :faint]) 339 340 [ 341 UI.edge([outer_color, :faint]), 342 :reset, 343 String.duplicate(" ", @indent - 2), 344 " {", 345 :cyan, 346 check_name, 347 :reset, 348 ", ", 349 :cyan, 350 "false", 351 :reset, 352 "}" 353 ] 354 |> UI.puts() 355 356 UI.puts_edge([outer_color, :faint]) 357 358 [ 359 UI.edge([outer_color, :faint]), 360 :reset, 361 String.duplicate(" ", @indent - 2), 362 "There are no other configuration options." 363 ] 364 |> UI.puts() 365 366 UI.puts_edge([outer_color, :faint]) 367 end 368 369 def print_params_explanation(outer_color, check_name, keywords, defaults) do 370 [ 371 UI.edge([outer_color, :faint]), 372 :reset, 373 String.duplicate(" ", @indent - 2), 374 "To configure this check, use this tuple" 375 ] 376 |> UI.puts() 377 378 UI.puts_edge([outer_color, :faint]) 379 380 [ 381 UI.edge([outer_color, :faint]), 382 :reset, 383 String.duplicate(" ", @indent - 2), 384 " {", 385 :cyan, 386 check_name, 387 :reset, 388 ", ", 389 :cyan, 390 :faint, 391 "<params>", 392 :reset, 393 "}" 394 ] 395 |> UI.puts() 396 397 UI.puts_edge([outer_color, :faint]) 398 399 [ 400 UI.edge([outer_color, :faint]), 401 :reset, 402 String.duplicate(" ", @indent - 2), 403 "with ", 404 :cyan, 405 :faint, 406 "<params>", 407 :reset, 408 " being ", 409 :cyan, 410 "false", 411 :reset, 412 " or any combination of these keywords:" 413 ] 414 |> UI.puts() 415 416 UI.puts_edge([outer_color, :faint]) 417 418 params_indent = get_params_indent(keywords, @params_min_indent) 419 420 keywords 421 |> Enum.each(fn {param, text} -> 422 [head | tail] = String.split(text, "\n") 423 424 [ 425 UI.edge([outer_color, :faint]), 426 :reset, 427 String.duplicate(" ", @indent - 2), 428 :cyan, 429 " #{param}:" |> String.pad_trailing(params_indent + 3), 430 :reset, 431 head 432 ] 433 |> UI.puts() 434 435 tail 436 |> List.wrap() 437 |> Enum.each(fn line -> 438 [ 439 UI.edge([outer_color, :faint]), 440 :reset, 441 String.duplicate(" ", @indent - 2), 442 :cyan, 443 String.pad_trailing("", params_indent + 3), 444 :reset, 445 line 446 ] 447 |> UI.puts() 448 end) 449 450 default = defaults[param] 451 452 if default do 453 default_text = "(defaults to #{inspect(default)})" 454 455 [ 456 UI.edge([outer_color, :faint]), 457 :reset, 458 String.duplicate(" ", @indent - 2), 459 :cyan, 460 " " |> String.pad_trailing(params_indent + 3), 461 :reset, 462 :faint, 463 default_text 464 ] 465 |> UI.puts() 466 end 467 end) 468 end 469 470 defp get_params_indent(keywords, min_indent) do 471 params_indent = 472 Enum.reduce(keywords, min_indent, fn {param, _text}, current -> 473 size = 474 param 475 |> to_string 476 |> String.length() 477 478 if size > current do 479 size 480 else 481 current 482 end 483 end) 484 485 # Round up to the next multiple of 2 486 (trunc(params_indent / 2) + 1) * 2 487 end 488 489 defp print_issue_column(column, trigger, outer_color, inner_color) do 490 offset = 0 491 # column is one-based 492 x = max(column - offset - 1, 0) 493 494 w = 495 if is_nil(trigger) do 496 1 497 else 498 trigger 499 |> to_string() 500 |> String.length() 501 end 502 503 [ 504 UI.edge([outer_color, :faint], @indent), 505 inner_color, 506 String.duplicate(" ", x), 507 :faint, 508 String.duplicate("^", w) 509 ] 510 |> UI.puts() 511 end 512 513 defp print_issue_line_no( 514 term_width, 515 line_no, 516 column, 517 trigger, 518 related_code, 519 outer_color, 520 inner_color 521 ) do 522 UI.puts_edge([outer_color, :faint]) 523 524 [ 525 UI.edge([outer_color, :faint]), 526 :reset, 527 :color239, 528 String.duplicate(" ", @indent - 5), 529 "__ CODE IN QUESTION" 530 ] 531 |> UI.puts() 532 533 UI.puts_edge([outer_color, :faint]) 534 535 code_color = :faint 536 537 print_source_line( 538 related_code, 539 line_no - 2, 540 term_width, 541 code_color, 542 outer_color 543 ) 544 545 print_source_line( 546 related_code, 547 line_no - 1, 548 term_width, 549 code_color, 550 outer_color 551 ) 552 553 print_source_line( 554 related_code, 555 line_no, 556 term_width, 557 [:cyan, :bright], 558 outer_color 559 ) 560 561 if column, do: print_issue_column(column, trigger, outer_color, inner_color) 562 563 print_source_line( 564 related_code, 565 line_no + 1, 566 term_width, 567 code_color, 568 outer_color 569 ) 570 571 print_source_line( 572 related_code, 573 line_no + 2, 574 term_width, 575 code_color, 576 outer_color 577 ) 578 end 579 580 defp print_source_line(related_code, line_no, term_width, code_color, outer_color) do 581 line = 582 related_code 583 |> Enum.find_value(fn 584 {line_no2, line} when line_no2 == line_no -> line 585 _ -> nil 586 end) 587 588 if line do 589 line_no_str = 590 "#{line_no} " 591 |> String.pad_leading(@indent - 2) 592 593 [ 594 UI.edge([outer_color, :faint]), 595 :reset, 596 :faint, 597 line_no_str, 598 :reset, 599 code_color, 600 UI.truncate(line, term_width - @indent) 601 ] 602 |> UI.puts() 603 end 604 end 605 end