explain_command.ex (6148B)
1 defmodule Credo.CLI.Command.Explain.ExplainCommand do 2 @moduledoc false 3 4 use Credo.CLI.Command, 5 short_description: "Show code object and explain why it is/might be an issue", 6 cli_switches: Credo.CLI.Command.Suggest.SuggestCommand.cli_switches() 7 8 alias Credo.Check 9 alias Credo.CLI.Command.Explain.ExplainOutput, as: Output 10 alias Credo.CLI.Filename 11 alias Credo.CLI.Task 12 alias Credo.Execution 13 alias Credo.Issue 14 alias Credo.SourceFile 15 16 def init(exec) do 17 exec 18 |> Execution.put_pipeline(__MODULE__.ExplainIssue, 19 validate_given_location: [ 20 {__MODULE__.ExplainIssuePreCheck, []} 21 ], 22 load_and_validate_source_files: [ 23 {Task.LoadAndValidateSourceFiles, []} 24 ], 25 prepare_analysis: [ 26 {Task.PrepareChecksToRun, []} 27 ], 28 run_analysis: [ 29 {Task.RunChecks, []} 30 ], 31 filter_issues: [ 32 {Task.SetRelevantIssues, []} 33 ], 34 print_explanation: [ 35 {__MODULE__.ExplainIssue, []} 36 ] 37 ) 38 |> Execution.put_pipeline(__MODULE__.ExplainCheck, 39 print_explanation: [ 40 {__MODULE__.ExplainCheck, []} 41 ] 42 ) 43 end 44 45 @doc false 46 def call(%Execution{help: true} = exec, _opts), do: Output.print_help(exec) 47 48 def call(exec, _opts) do 49 filename = get_filename_from_args(exec) 50 51 cond do 52 Filename.contains_line_no?(filename) -> 53 Execution.run_pipeline(exec, __MODULE__.ExplainIssue) 54 55 Check.defined?("Elixir.#{filename}") -> 56 Execution.run_pipeline(exec, __MODULE__.ExplainCheck) 57 58 true -> 59 Output.print_help(exec) 60 end 61 end 62 63 @doc false 64 def get_filename_from_args(exec) do 65 exec.cli_options.args 66 |> List.wrap() 67 |> List.first() 68 end 69 70 defmodule ExplainCheck do 71 use Credo.Execution.Task 72 73 alias Credo.CLI.Command.Explain.ExplainCommand 74 75 def call(exec, _opts) do 76 check_name = ExplainCommand.get_filename_from_args(exec) 77 check = :"Elixir.#{check_name}" 78 explanations = [cast_to_explanation(check)] 79 80 Output.print_after_info(explanations, exec, nil, nil) 81 82 exec 83 end 84 85 defp cast_to_explanation(check) do 86 %{ 87 category: check.category, 88 check: check, 89 explanation_for_issue: check.explanation, 90 priority: check.base_priority 91 } 92 end 93 end 94 95 defmodule ExplainIssuePreCheck do 96 use Credo.Execution.Task 97 98 alias Credo.CLI.Command.Explain.ExplainCommand 99 100 def call(exec, _opts) do 101 filename_with_location = ExplainCommand.get_filename_from_args(exec) 102 working_dir = Execution.working_dir(exec) 103 104 filename = 105 filename_with_location 106 |> String.split(":") 107 |> List.first() 108 |> Path.expand() 109 110 if path_contains_file?(working_dir, filename) do 111 exec 112 else 113 Execution.halt(exec, """ 114 Given location is not part of the working dir. 115 116 Location: #{filename_with_location} 117 Working dir: #{working_dir} 118 """) 119 end 120 end 121 122 # def error(exec, _opts) do 123 # halt_message = Execution.get_halt_message(exec) 124 125 # UI.warn([:red, "** (explain) ", halt_message]) 126 127 # exec 128 # end 129 130 defp path_contains_file?(path, filename) do 131 case Path.relative_to(filename, path) do 132 ^filename -> false 133 _ -> true 134 end 135 end 136 end 137 138 defmodule ExplainIssue do 139 use Credo.Execution.Task 140 141 alias Credo.CLI.Command.Explain.ExplainCommand 142 143 def call(exec, _opts) do 144 filename = ExplainCommand.get_filename_from_args(exec) 145 146 source_files = Execution.get_source_files(exec) 147 148 filename 149 |> String.split(":") 150 |> print_result(source_files, exec) 151 end 152 153 def print_result([filename], source_files, exec) do 154 print_result([filename, nil, nil], source_files, exec) 155 end 156 157 def print_result([filename, line_no], source_files, exec) do 158 print_result([filename, line_no, nil], source_files, exec) 159 end 160 161 def print_result([filename, line_no, column], source_files, exec) do 162 source_file = Enum.find(source_files, &(&1.filename == filename)) 163 164 if source_file do 165 explanations = 166 exec 167 |> Execution.get_issues(source_file.filename) 168 |> filter_issues(line_no, column) 169 |> Enum.map(&cast_to_explanation(&1, source_file)) 170 171 Output.print_after_info(explanations, exec, line_no, column) 172 173 exec 174 else 175 Execution.halt(exec, "Could not find source file: #{filename}") 176 end 177 end 178 179 defp cast_to_explanation(issue, source_file) do 180 %{ 181 category: issue.category, 182 check: issue.check, 183 column: issue.column, 184 explanation_for_issue: issue.check.explanation, 185 filename: issue.filename, 186 line_no: issue.line_no, 187 message: issue.message, 188 priority: issue.priority, 189 related_code: find_related_code(source_file, issue.line_no), 190 scope: issue.scope, 191 trigger: issue.trigger 192 } 193 end 194 195 defp find_related_code(source_file, line_no) do 196 [ 197 get_source_line(source_file, line_no - 2), 198 get_source_line(source_file, line_no - 1), 199 get_source_line(source_file, line_no), 200 get_source_line(source_file, line_no + 1), 201 get_source_line(source_file, line_no + 2) 202 ] 203 |> Enum.reject(&is_nil/1) 204 end 205 206 defp get_source_line(_, line_no) when line_no < 1 do 207 nil 208 end 209 210 defp get_source_line(source_file, line_no) do 211 line = SourceFile.line_at(source_file, line_no) 212 213 if line do 214 {line_no, line} 215 end 216 end 217 218 defp filter_issues(issues, line_no, nil) do 219 line_no = line_no |> String.to_integer() 220 issues |> Enum.filter(&filter_issue(&1, line_no, nil)) 221 end 222 223 defp filter_issues(issues, line_no, column) do 224 line_no = line_no |> String.to_integer() 225 column = column |> String.to_integer() 226 227 issues |> Enum.filter(&filter_issue(&1, line_no, column)) 228 end 229 230 defp filter_issue(%Issue{line_no: a, column: b}, a, b), do: true 231 defp filter_issue(%Issue{line_no: a}, a, _), do: true 232 defp filter_issue(_, _, _), do: false 233 end 234 end