plugin.ex (13692B)
1 defmodule Credo.Plugin do 2 @moduledoc """ 3 Plugins are module which can provide additional functionality to Credo. 4 5 A plugin is basically just a module that provides an `init/1` callback. 6 7 defmodule CredoDemoPlugin do 8 def init(exec) do 9 # but what do we do here?? 10 exec 11 end 12 end 13 14 The `Credo.Plugin` module provides a number of functions for extending Credo's core features. 15 16 defmodule CredoDemoPlugin do 17 @config_file File.read!(".credo.exs") 18 19 import Credo.Plugin 20 21 def init(exec) do 22 exec 23 |> register_default_config(@config_file) 24 |> register_command("demo", CredoDemoPlugin.DemoCommand) 25 |> register_cli_switch(:castle, :string, :X) 26 |> prepend_task(:set_default_command, CredoDemoPlugin.SetDemoAsDefaultCommand) 27 end 28 end 29 30 """ 31 32 require Credo.Execution 33 34 pipeline_main_group_names_as_bullet_list = """ 35 - `:parse_cli_options` 36 - `:initialize_plugins` 37 - `:determine_command` 38 - `:set_default_command` 39 - `:initialize_command` 40 - `:parse_cli_options_final` 41 - `:validate_cli_options` 42 - `:convert_cli_options_to_config` 43 - `:resolve_config` 44 - `:validate_config` 45 - `:run_command` 46 - `:halt_execution` 47 """ 48 49 pipeline_existing_commands_group_names_as_bullet_list = """ 50 - `Credo.CLI.Command.Suggest.SuggestCommand` (run via `mix credo suggest`) 51 - `:load_and_validate_source_files` 52 - `:prepare_analysis` 53 - `:print_before_analysis` 54 - `:run_analysis` 55 - `:filter_issues` 56 - `:print_after_analysis` 57 58 - `Credo.CLI.Command.List.ListCommand` (run via `mix credo list`) 59 - `:load_and_validate_source_files` 60 - `:prepare_analysis` 61 - `:print_before_analysis` 62 - `:run_analysis` 63 - `:filter_issues` 64 - `:print_after_analysis` 65 66 - `Credo.CLI.Command.Diff.DiffCommand` (run via `mix credo diff`) 67 - `:load_and_validate_source_files` 68 - `:prepare_analysis` 69 - `:print_previous_analysis` 70 - `:run_analysis` 71 - `:filter_issues` 72 - `:print_after_analysis` 73 - `:filter_issues_for_exit_status` 74 75 - `Credo.CLI.Command.Info.InfoCommand` (run via `mix credo info`) 76 - `:load_and_validate_source_files` 77 - `:prepare_analysis` 78 - `:print_info` 79 """ 80 81 alias Credo.Execution 82 83 @doc """ 84 Appends a `Credo.Execution.Task` module to Credo's main execution pipeline. 85 86 Credo's execution pipeline consists of several steps, each with a group of tasks, which you can hook into. 87 88 Appending tasks to these steps is easy: 89 90 # credo_demo_plugin.ex 91 defmodule CredoDemoPlugin do 92 import Credo.Plugin 93 94 def init(exec) do 95 append_task(exec, :set_default_command, CredoDemoPlugin.SetDemoAsDefaultCommand) 96 end 97 end 98 99 Note how `Credo.Plugin.append_task/3` takes two arguments after the `Credo.Execution` struct: the name of the group to be modified and the module that should be executed. 100 101 The group names of Credo's main pipeline are: 102 103 #{pipeline_main_group_names_as_bullet_list} 104 105 The module appended to these groups should use `Credo.Execution.Task`: 106 107 # credo_demo_plugin/set_demo_as_default_command.ex 108 defmodule CredoDemoPlugin.SetDemoAsDefaultCommand do 109 use Credo.Execution.Task 110 111 alias Credo.CLI.Options 112 113 def call(exec, _opts) do 114 set_command(exec, exec.cli_options.command || "demo") 115 end 116 117 defp set_command(exec, command) do 118 %Execution{exec | cli_options: %Options{exec.cli_options | command: command}} 119 end 120 end 121 122 This example would have the effect that typing `mix credo` would no longer run the built-in `Suggest` command, but rather our plugin's `Demo` command. 123 """ 124 def append_task(%Execution{initializing_plugin: plugin_mod} = exec, group_name, task_mod) do 125 Execution.append_task(exec, plugin_mod, nil, group_name, task_mod) 126 end 127 128 @doc """ 129 Appends a `Credo.Execution.Task` module to the execution pipeline of an existing Command. 130 131 Credo's commands can also have an execution pipeline of their own, which is executed when the command is used and which you can hook into as well. 132 133 Appending tasks to these steps is easy: 134 135 # credo_demo_plugin.ex 136 defmodule CredoDemoPlugin do 137 import Credo.Plugin 138 139 def init(exec) do 140 append_task(exec, Credo.CLI.Command.Suggest.SuggestCommand, :print_after_analysis, CredoDemoPlugin.WriteFile) 141 end 142 end 143 144 Note how `Credo.Plugin.append_task/4` takes three arguments after the `Credo.Execution` struct: the pipeline and the name of the group to be modified and the module that should be executed. 145 146 Here are the pipeline keys and group names: 147 148 #{pipeline_existing_commands_group_names_as_bullet_list} 149 150 The module appended to these groups should use `Credo.Execution.Task`: 151 152 # credo_demo_plugin/write_file.ex 153 defmodule CredoDemoPlugin.WriteFile do 154 use Credo.Execution.Task 155 156 alias Credo.CLI.Options 157 158 def call(exec, _opts) do 159 issue_count = exec |> Execution.get_issues() |> Enum.count 160 File.write!("demo.json", ~q({"issue_count": \#{issue_count}})) 161 162 exec 163 end 164 end 165 166 This example would have the effect that running `mix credo suggest` would write the issue count in a JSON file. 167 """ 168 def append_task( 169 %Execution{initializing_plugin: plugin_mod} = exec, 170 pipeline_key, 171 group_name, 172 task_mod 173 ) do 174 Execution.append_task(exec, plugin_mod, pipeline_key, group_name, task_mod) 175 end 176 177 @doc """ 178 Prepends a `Credo.Execution.Task` module to Credo's main execution pipeline. 179 180 Credo's execution pipeline consists of several steps, each with a group of tasks, which you can hook into. 181 182 Prepending tasks to these steps is easy: 183 184 # credo_demo_plugin.ex 185 defmodule CredoDemoPlugin do 186 import Credo.Plugin 187 188 def init(exec) do 189 prepend_task(exec, :set_default_command, CredoDemoPlugin.SetDemoAsDefaultCommand) 190 end 191 end 192 193 Note how `Credo.Plugin.prepend_task/3` takes two arguments after the `Credo.Execution` struct: the name of the group to be modified and the module that should be executed. 194 195 The group names of Credo's main pipeline are: 196 197 #{pipeline_main_group_names_as_bullet_list} 198 199 The module prepended to these groups should use `Credo.Execution.Task`: 200 201 # credo_demo_plugin/set_demo_as_default_command.ex 202 defmodule CredoDemoPlugin.SetDemoAsDefaultCommand do 203 use Credo.Execution.Task 204 205 alias Credo.CLI.Options 206 207 def call(exec, _opts) do 208 set_command(exec, exec.cli_options.command || "demo") 209 end 210 211 defp set_command(exec, command) do 212 %Execution{exec | cli_options: %Options{exec.cli_options | command: command}} 213 end 214 end 215 216 This example would have the effect that typing `mix credo` would no longer run the built-in `Suggest` command, but rather our plugin's `Demo` command. 217 """ 218 def prepend_task(%Execution{initializing_plugin: plugin_mod} = exec, group_name, task_mod) do 219 Execution.prepend_task(exec, plugin_mod, nil, group_name, task_mod) 220 end 221 222 @doc """ 223 Prepends a `Credo.Execution.Task` module to the execution pipeline of an existing Command. 224 225 Credo's commands can also have an execution pipeline of their own, which is executed when the command is used and which you can hook into as well. 226 227 Prepending tasks to these steps is easy: 228 229 # credo_demo_plugin.ex 230 defmodule CredoDemoPlugin do 231 import Credo.Plugin 232 233 def init(exec) do 234 prepend_task(exec, Credo.CLI.Command.Suggest.SuggestCommand, :print_after_analysis, CredoDemoPlugin.WriteFile) 235 end 236 end 237 238 Note how `Credo.Plugin.prepend_task/4` takes three arguments after the `Credo.Execution` struct: the pipeline and the name of the group to be modified and the module that should be executed. 239 240 Here are the pipeline keys and group names: 241 242 #{pipeline_existing_commands_group_names_as_bullet_list} 243 244 The module prepended to these groups should use `Credo.Execution.Task`: 245 246 # credo_demo_plugin/write_file.ex 247 defmodule CredoDemoPlugin.WriteFile do 248 use Credo.Execution.Task 249 250 alias Credo.CLI.Options 251 252 def call(exec, _opts) do 253 issue_count = exec |> Execution.get_issues() |> Enum.count 254 File.write!("demo.json", ~q({"issue_count": \#{issue_count}})) 255 256 exec 257 end 258 end 259 260 This example would have the effect that running `mix credo suggest` would write the issue count in a JSON file. 261 """ 262 def prepend_task( 263 %Execution{initializing_plugin: plugin_mod} = exec, 264 pipeline_key, 265 group_name, 266 task_mod 267 ) do 268 Execution.prepend_task(exec, plugin_mod, pipeline_key, group_name, task_mod) 269 end 270 271 @doc """ 272 Adds a CLI switch to Credo. 273 274 For demo purposes, we are writing a command called `demo` (see `register_command/3`): 275 276 # credo_demo_plugin/demo_command.ex 277 defmodule CredoDemoPlugin do 278 import Credo.Plugin 279 280 def init(exec) do 281 exec 282 |> register_command("demo", CredoDemoPlugin.DemoCommand) 283 end 284 end 285 286 # credo_demo_plugin/demo_command.ex 287 defmodule CredoDemoPlugin.DemoCommand do 288 alias Credo.CLI.Output.UI 289 alias Credo.Execution 290 291 def call(exec, _) do 292 castle = Execution.get_plugin_param(exec, CredoDemoPlugin, :castle) 293 294 UI.puts("By the power of \#{castle}!") 295 296 exec 297 end 298 end 299 300 Since Plugins can be configured by params in `.credo.exs`, we can add the `:castle` param: 301 302 # .credo.exs 303 {CredoDemoPlugin, [castle: "Grayskull"]} 304 305 And get the following output: 306 307 ```bash 308 $ mix credo demo 309 By the power of Grayskull! 310 ``` 311 312 Plugins can provide custom CLI options as well, so we can do something like: 313 314 ```bash 315 $ mix credo demo --castle Winterfell 316 Unknown switch: --castle 317 ``` 318 319 Registering a custom CLI switch for this is easy: 320 321 defmodule CredoDemoPlugin do 322 import Credo.Plugin 323 324 def init(exec) do 325 exec 326 |> register_command("demo", CredoDemoPlugin.DemoCommand) 327 |> register_cli_switch(:castle, :string, :X) 328 end 329 end 330 331 Every registered CLI switch is automatically converted into a plugin param of the same name, which is why we get the following output: 332 333 ```bash 334 $ mix credo demo --castle Winterfell 335 By the power of Winterfell! 336 337 $ mix credo demo -X Camelot 338 By the power of Camelot! 339 ``` 340 341 Plugin authors can also provide a function to control the plugin param's name and value more granularly: 342 343 defmodule CredoDemoPlugin do 344 import Credo.Plugin 345 346 def init(exec) do 347 register_cli_switch(exec, :kastle, :string, :X, fn(switch_value) -> 348 {:castle, String.upcase(switch_value)} 349 end) 350 end 351 end 352 353 And get the following output: 354 355 ```bash 356 $ mix credo demo --kastle Winterfell 357 By the power of WINTERFELL! 358 ``` 359 360 """ 361 def register_cli_switch( 362 %Execution{initializing_plugin: plugin_mod} = exec, 363 name, 364 type, 365 alias_name \\ nil, 366 convert_to_param \\ true 367 ) do 368 exec 369 |> Execution.put_cli_switch(plugin_mod, name, type) 370 |> Execution.put_cli_switch_alias(plugin_mod, name, alias_name) 371 |> Execution.put_cli_switch_plugin_param_converter(plugin_mod, name, convert_to_param) 372 end 373 374 @doc ~S""" 375 Registers and initializes a Command module with a given `name`. 376 377 ## Add new commands 378 379 Commands are just modules with a call function and adding new commands is easy. 380 381 # credo_demo_plugin.ex 382 defmodule CredoDemoPlugin do 383 import Credo.Plugin 384 385 def init(exec) do 386 register_command(exec, "demo", CredoDemoPlugin.DemoCommand) 387 end 388 end 389 390 # credo_demo_plugin/demo_command.ex 391 defmodule CredoDemoPlugin.DemoCommand do 392 alias Credo.CLI.Output.UI 393 alias Credo.Execution 394 395 def call(exec, _) do 396 castle = Execution.get_plugin_param(exec, CredoDemoPlugin, :castle) 397 398 UI.puts("By the power of #{castle}!") 399 400 exec 401 end 402 end 403 404 Users can use this command by typing 405 406 ```bash 407 $ mix credo demo 408 By the power of ! 409 ``` 410 411 ## Override an existing command 412 413 Since commands are just modules with a call function, overriding existing commands is easy. 414 415 defmodule CredoDemoPlugin do 416 import Credo.Plugin 417 418 def init(exec) do 419 register_command(exec, "explain", CredoDemoPlugin.MyBetterExplainCommand) 420 end 421 end 422 423 This example would have the effect that typing `mix credo lib/my_file.ex:42` would no longer run the built-in `Explain` command, but rather our plugin's `MyBetterExplain` command. 424 """ 425 def register_command(%Execution{initializing_plugin: plugin_mod} = exec, name, command_mod) do 426 Execution.put_command(exec, plugin_mod, name, command_mod) 427 end 428 429 @doc """ 430 Registers the contents of a config file. 431 432 This registers the contents of a config file as default config, loading it after Credo's own default config but before the [config files loaded from the current working directory](config_file.html#transitive-configuration-files). 433 434 defmodule CredoDemoPlugin do 435 @config_file File.read!(".credo.exs") 436 437 import Credo.Plugin 438 439 def init(exec) do 440 register_default_config(exec, @config_file) 441 end 442 end 443 """ 444 def register_default_config( 445 %Execution{initializing_plugin: plugin_mod} = exec, 446 config_file_string 447 ) do 448 Execution.append_config_file(exec, {:plugin, plugin_mod, config_file_string}) 449 end 450 end