zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

check.ex (19610B)


      1 defmodule Credo.Check do
      2   @moduledoc """
      3   `Check` modules represent the checks which are run during Credo's analysis.
      4 
      5   Example:
      6 
      7       defmodule MyCheck do
      8         use Credo.Check, category: :warning, base_priority: :high
      9 
     10         def run(%SourceFile{} = source_file, params) do
     11           #
     12         end
     13       end
     14 
     15   The check can be configured by passing the following
     16   options to `use Credo.Check`:
     17 
     18   - `:base_priority`  Sets the checks's base priority (`:low`, `:normal`, `:high`, `:higher` or `:ignore`).
     19   - `:category`       Sets the check's category (`:consistency`, `:design`, `:readability`, `:refactor`  or `:warning`).
     20   - `:elixir_version` Sets the check's version requirement for Elixir (defaults to `>= 0.0.1`).
     21   - `:explanations`   Sets explanations displayed for the check, e.g.
     22 
     23       ```elixir
     24       [
     25         check: "...",
     26         params: [
     27           param1: "Your favorite number",
     28           param2: "Online/Offline mode"
     29         ]
     30       ]
     31       ```
     32 
     33   - `:param_defaults` Sets the default values for the check's params (e.g. `[param1: 42, param2: "offline"]`)
     34   - `:tags`           Sets the tags for this check (list of atoms, e.g. `[:tag1, :tag2]`)
     35 
     36   Please also note that these options to `use Credo.Check` are just a convenience to implement the `Credo.Check`
     37   behaviour. You can implement any of these by hand:
     38 
     39       defmodule MyCheck do
     40         use Credo.Check
     41 
     42         def category, do: :warning
     43 
     44         def base_priority, do: :high
     45 
     46         def explanations do
     47           [
     48             check: "...",
     49             params: [
     50               param1: "Your favorite number",
     51               param2: "Online/Offline mode"
     52             ]
     53           ]
     54         end
     55 
     56         def param_defaults, do: [param1: 42, param2: "offline"]
     57 
     58         def run(%SourceFile{} = source_file, params) do
     59           #
     60         end
     61       end
     62 
     63   The `run/2` function of a Check module takes two parameters: a source file and a list of parameters for the check.
     64   It has to return a list of found issues.
     65   """
     66 
     67   @doc """
     68   Runs the current check on all `source_files` by calling `run_on_source_file/3`.
     69 
     70   If you are developing a check that has to run on all source files, you can overwrite `run_on_all_source_files/3`:
     71 
     72       defmodule MyCheck do
     73         use Credo.Check
     74 
     75         def run_on_all_source_files(exec, source_files, params) do
     76           issues =
     77             source_files
     78             |> do_something_crazy()
     79             |> do_something_crazier()
     80 
     81           append_issues_and_timings(exec, issues)
     82 
     83           :ok
     84         end
     85       end
     86 
     87   Check out Credo's checks from the consistency category for examples of these kinds of checks.
     88   """
     89   @callback run_on_all_source_files(
     90               exec :: Credo.Execution.t(),
     91               source_files :: list(Credo.SourceFile.t()),
     92               params :: Keyword.t()
     93             ) :: :ok
     94 
     95   @doc """
     96   Runs the current check on a single `source_file` and appends the resulting issues to the current `exec`.
     97   """
     98   @callback run_on_source_file(
     99               exec :: Credo.Execution.t(),
    100               source_file :: Credo.SourceFile.t(),
    101               params :: Keyword.t()
    102             ) :: :ok
    103 
    104   @callback run(source_file :: Credo.SourceFile.t(), params :: Keyword.t()) ::
    105               list(Credo.Issue.t())
    106 
    107   @doc """
    108   Returns the base priority for the check.
    109 
    110   This can be one of `:higher`, `:high`, `:normal`, `:low` or `:ignore`
    111   (technically it can also be  or an integer, but these are internal representations although that is not recommended).
    112   """
    113   @callback base_priority() :: :higher | :high | :normal | :low | :ignore | integer
    114 
    115   @doc """
    116   Returns the category for the check.
    117   """
    118   @callback category() :: atom
    119 
    120   @doc """
    121   Returns the required Elixir version for the check.
    122   """
    123   @callback elixir_version() :: String.t()
    124 
    125   @doc """
    126   Returns the exit status for the check.
    127   """
    128   @callback exit_status() :: integer
    129 
    130   @doc """
    131   Returns the explanations for the check and params as a keyword list.
    132   """
    133   @callback explanations() :: Keyword.t()
    134 
    135   @doc """
    136   Returns the default values for the check's params as a keyword list.
    137   """
    138   @callback param_defaults() :: Keyword.t()
    139 
    140   # @callback run(source_file :: Credo.SourceFile.t, params :: Keyword.t) :: list()
    141 
    142   @doc """
    143   Returns whether or not this check runs on all source files.
    144   """
    145   @callback run_on_all?() :: boolean
    146 
    147   @doc """
    148   Returns the tags for the check.
    149   """
    150   @callback tags() :: list(atom)
    151 
    152   @doc false
    153   @callback format_issue(issue_meta :: Credo.IssueMeta.t(), opts :: Keyword.t()) ::
    154               Credo.Issue.t()
    155 
    156   @base_category_exit_status_map %{
    157     consistency: 1,
    158     design: 2,
    159     readability: 4,
    160     refactor: 8,
    161     warning: 16
    162   }
    163 
    164   alias Credo.Check
    165   alias Credo.Check.Params
    166   alias Credo.Code.Scope
    167   alias Credo.Issue
    168   alias Credo.IssueMeta
    169   alias Credo.Priority
    170   alias Credo.Service.SourceFileScopes
    171   alias Credo.Severity
    172   alias Credo.SourceFile
    173 
    174   @valid_use_opts [
    175     :base_priority,
    176     :category,
    177     :elixir_version,
    178     :exit_status,
    179     :explanations,
    180     :param_defaults,
    181     :run_on_all,
    182     :tags
    183   ]
    184 
    185   @doc false
    186   defmacro __using__(opts) do
    187     Enum.each(opts, fn
    188       {key, _name} when key not in @valid_use_opts ->
    189         raise "Could not find key `#{key}` in #{inspect(@valid_use_opts)}"
    190 
    191       _ ->
    192         nil
    193     end)
    194 
    195     def_base_priority =
    196       if opts[:base_priority] do
    197         quote do
    198           @impl true
    199           def base_priority, do: unquote(opts[:base_priority])
    200         end
    201       else
    202         quote do
    203           @impl true
    204           def base_priority, do: 0
    205         end
    206       end
    207 
    208     def_category =
    209       if opts[:category] do
    210         quote do
    211           @impl true
    212           def category, do: unquote(category_body(opts[:category]))
    213         end
    214       else
    215         quote do
    216           @impl true
    217           def category, do: unquote(category_body(nil))
    218         end
    219       end
    220 
    221     def_elixir_version =
    222       if opts[:elixir_version] do
    223         quote do
    224           @impl true
    225           def elixir_version do
    226             unquote(opts[:elixir_version])
    227           end
    228         end
    229       else
    230         quote do
    231           @impl true
    232           def elixir_version, do: ">= 0.0.1"
    233         end
    234       end
    235 
    236     def_exit_status =
    237       if opts[:exit_status] do
    238         quote do
    239           @impl true
    240           def exit_status do
    241             unquote(opts[:exit_status])
    242           end
    243         end
    244       else
    245         quote do
    246           @impl true
    247           def exit_status, do: Credo.Check.to_exit_status(category())
    248         end
    249       end
    250 
    251     def_run_on_all? =
    252       if opts[:run_on_all] do
    253         quote do
    254           @impl true
    255           def run_on_all?, do: unquote(opts[:run_on_all] == true)
    256         end
    257       else
    258         quote do
    259           @impl true
    260           def run_on_all?, do: false
    261         end
    262       end
    263 
    264     def_param_defaults =
    265       if opts[:param_defaults] do
    266         quote do
    267           @impl true
    268           def param_defaults, do: unquote(opts[:param_defaults])
    269         end
    270       end
    271 
    272     def_explanations =
    273       if opts[:explanations] do
    274         quote do
    275           @impl true
    276           def explanations do
    277             unquote(opts[:explanations])
    278           end
    279         end
    280       end
    281 
    282     def_tags =
    283       quote do
    284         @impl true
    285         def tags do
    286           unquote(opts[:tags] || [])
    287         end
    288       end
    289 
    290     quote do
    291       @moduledoc unquote(moduledoc(opts))
    292       @behaviour Credo.Check
    293       @before_compile Credo.Check
    294 
    295       @use_deprecated_run_on_all? false
    296 
    297       alias Credo.Check
    298       alias Credo.Check.Params
    299       alias Credo.CLI.ExitStatus
    300       alias Credo.CLI.Output.UI
    301       alias Credo.Execution
    302       alias Credo.Execution.ExecutionTiming
    303       alias Credo.Issue
    304       alias Credo.IssueMeta
    305       alias Credo.Priority
    306       alias Credo.Severity
    307       alias Credo.SourceFile
    308 
    309       unquote(def_base_priority)
    310       unquote(def_category)
    311       unquote(def_elixir_version)
    312       unquote(def_exit_status)
    313       unquote(def_run_on_all?)
    314       unquote(def_param_defaults)
    315       unquote(def_explanations)
    316       unquote(def_tags)
    317 
    318       @impl true
    319       def format_issue(issue_meta, issue_options) do
    320         Check.format_issue(
    321           issue_meta,
    322           issue_options,
    323           __MODULE__
    324         )
    325       end
    326 
    327       @doc false
    328       @impl true
    329       def run_on_all_source_files(exec, source_files, params \\ [])
    330 
    331       @impl true
    332       def run_on_all_source_files(exec, source_files, params) do
    333         if function_exported?(__MODULE__, :run, 3) do
    334           IO.warn(
    335             "Defining `run(source_files, exec, params)` for checks that run on all source files is deprecated. " <>
    336               "Define `run_on_all_source_files(exec, source_files, params)` instead."
    337           )
    338 
    339           apply(__MODULE__, :run, [source_files, exec, params])
    340         else
    341           do_run_on_all_source_files(exec, source_files, params)
    342         end
    343       end
    344 
    345       defp do_run_on_all_source_files(exec, source_files, params) do
    346         source_files
    347         |> Enum.map(&Task.async(fn -> run_on_source_file(exec, &1, params) end))
    348         |> Enum.each(&Task.await(&1, :infinity))
    349 
    350         :ok
    351       end
    352 
    353       @doc false
    354       @impl true
    355       def run_on_source_file(exec, source_file, params \\ [])
    356 
    357       def run_on_source_file(%Execution{debug: true} = exec, source_file, params) do
    358         ExecutionTiming.run(&do_run_on_source_file/3, [exec, source_file, params])
    359         |> ExecutionTiming.append(exec,
    360           task: exec.current_task,
    361           check: __MODULE__,
    362           filename: source_file.filename
    363         )
    364       end
    365 
    366       def run_on_source_file(exec, source_file, params) do
    367         do_run_on_source_file(exec, source_file, params)
    368       end
    369 
    370       defp do_run_on_source_file(exec, source_file, params) do
    371         issues =
    372           try do
    373             run(source_file, params)
    374           rescue
    375             error ->
    376               UI.warn("Error while running #{__MODULE__} on #{source_file.filename}")
    377 
    378               if exec.crash_on_error do
    379                 reraise error, __STACKTRACE__
    380               else
    381                 []
    382               end
    383           end
    384 
    385         append_issues_and_timings(issues, exec)
    386 
    387         :ok
    388       end
    389 
    390       @doc false
    391       @impl true
    392       def run(source_file, params)
    393 
    394       def run(%SourceFile{} = source_file, params) do
    395         throw("Implement me")
    396       end
    397 
    398       defoverridable Credo.Check
    399 
    400       defp append_issues_and_timings([] = _issues, exec) do
    401         exec
    402       end
    403 
    404       defp append_issues_and_timings([_ | _] = issues, exec) do
    405         Credo.Execution.ExecutionIssues.append(exec, issues)
    406       end
    407     end
    408   end
    409 
    410   @doc false
    411   defmacro __before_compile__(env) do
    412     quote do
    413       unquote(deprecated_def_default_params(env))
    414       unquote(deprecated_def_explanations(env))
    415 
    416       @doc false
    417       def param_names do
    418         Keyword.keys(param_defaults())
    419       end
    420 
    421       @deprecated "Use param_defaults/1 instead"
    422       @doc false
    423       def params_defaults do
    424         # deprecated - remove module attribute
    425         param_defaults()
    426       end
    427 
    428       @deprecated "Use param_names/1 instead"
    429       @doc false
    430       def params_names do
    431         param_names()
    432       end
    433 
    434       @deprecated "Use explanations()[:check] instead"
    435       @doc false
    436       def explanation do
    437         # deprecated - remove module attribute
    438         explanations()[:check]
    439       end
    440 
    441       @deprecated "Use explanations()[:params] instead"
    442       @doc false
    443       def explanation_for_params do
    444         # deprecated - remove module attribute
    445         explanations()[:params]
    446       end
    447     end
    448   end
    449 
    450   defp moduledoc(opts) do
    451     explanations = opts[:explanations]
    452 
    453     base_priority = opts_to_string(opts[:base_priority]) || 0
    454 
    455     # category = opts_to_string(opts[:category]) || to_string(__MODULE__)
    456 
    457     elixir_version_hint =
    458       if opts[:elixir_version] do
    459         elixir_version = opts_to_string(opts[:elixir_version])
    460 
    461         "requires Elixir `#{elixir_version}`"
    462       else
    463         "works with any version of Elixir"
    464       end
    465 
    466     check_doc = opts_to_string(explanations[:check])
    467     params = explanations[:params] |> opts_to_string() |> List.wrap()
    468     param_defaults = opts_to_string(opts[:param_defaults])
    469 
    470     params_doc =
    471       if params == [] do
    472         "*There are no specific parameters for this check.*"
    473       else
    474         param_explanation =
    475           Enum.map(params, fn {key, value} ->
    476             default_value = inspect(param_defaults[key], limit: :infinity)
    477 
    478             default_hint =
    479               if default_value do
    480                 "*This parameter defaults to* `#{default_value}`."
    481               end
    482 
    483             value = value |> String.split("\n") |> Enum.map_join("\n", &"  #{&1}")
    484 
    485             """
    486             ### `:#{key}`
    487 
    488             #{value}
    489 
    490             #{default_hint}
    491 
    492             """
    493           end)
    494 
    495         """
    496         Use the following parameters to configure this check:
    497 
    498         #{param_explanation}
    499 
    500         """
    501       end
    502 
    503     """
    504     This check has a base priority of `#{base_priority}` and #{elixir_version_hint}.
    505 
    506     ## Explanation
    507 
    508     #{check_doc}
    509 
    510     ## Check-Specific Parameters
    511 
    512     #{params_doc}
    513 
    514     ## General Parameters
    515 
    516     Like with all checks, [general params](check_params.html) can be applied.
    517 
    518     Parameters can be configured via the [`.credo.exs` config file](config_file.html).
    519     """
    520   end
    521 
    522   defp opts_to_string(value) do
    523     {result, _} =
    524       value
    525       |> Macro.to_string()
    526       |> Code.eval_string()
    527 
    528     result
    529   end
    530 
    531   defp deprecated_def_default_params(env) do
    532     default_params = Module.get_attribute(env.module, :default_params)
    533 
    534     if is_nil(default_params) do
    535       if not Module.defines?(env.module, {:param_defaults, 0}) do
    536         quote do
    537           @impl true
    538           def param_defaults, do: []
    539         end
    540       end
    541     else
    542       # deprecated - remove once we ditch @default_params
    543       quote do
    544         @impl true
    545         def param_defaults do
    546           @default_params
    547         end
    548       end
    549     end
    550   end
    551 
    552   defp deprecated_def_explanations(env) do
    553     defines_deprecated_explanation_module_attribute? =
    554       !is_nil(Module.get_attribute(env.module, :explanation))
    555 
    556     defines_deprecated_explanations_fun? = Module.defines?(env.module, {:explanations, 0})
    557 
    558     if defines_deprecated_explanation_module_attribute? do
    559       # deprecated - remove once we ditch @explanation
    560       quote do
    561         @impl true
    562         def explanations do
    563           @explanation
    564         end
    565       end
    566     else
    567       if !defines_deprecated_explanations_fun? do
    568         quote do
    569           @impl true
    570           def explanations, do: []
    571         end
    572       end
    573     end
    574   end
    575 
    576   def explanation_for(nil, _), do: nil
    577   def explanation_for(keywords, key), do: keywords[key]
    578 
    579   @doc """
    580   format_issue takes an issue_meta and returns an issue.
    581   The resulting issue can be made more explicit by passing the following
    582   options to `format_issue/2`:
    583 
    584   - `:priority`     Sets the issue's priority.
    585   - `:trigger`      Sets the issue's trigger.
    586   - `:line_no`      Sets the issue's line number. Tries to find `column` if `:trigger` is supplied.
    587   - `:column`       Sets the issue's column.
    588   - `:exit_status`  Sets the issue's exit_status.
    589   - `:severity`     Sets the issue's severity.
    590   """
    591   def format_issue(issue_meta, opts, check) do
    592     params = IssueMeta.params(issue_meta)
    593     issue_category = Params.category(params, check)
    594     issue_base_priority = Params.priority(params, check)
    595 
    596     format_issue(issue_meta, opts, issue_category, issue_base_priority, check)
    597   end
    598 
    599   @doc false
    600   def format_issue(issue_meta, opts, issue_category, issue_priority, check) do
    601     source_file = IssueMeta.source_file(issue_meta)
    602     params = IssueMeta.params(issue_meta)
    603 
    604     priority = Priority.to_integer(issue_priority)
    605 
    606     exit_status_or_category = Params.exit_status(params, check) || issue_category
    607     exit_status = Check.to_exit_status(exit_status_or_category)
    608 
    609     line_no = opts[:line_no]
    610     trigger = opts[:trigger]
    611     column = opts[:column]
    612     severity = opts[:severity] || Severity.default_value()
    613 
    614     %Issue{
    615       priority: priority,
    616       filename: source_file.filename,
    617       message: opts[:message],
    618       trigger: trigger,
    619       line_no: line_no,
    620       column: column,
    621       severity: severity,
    622       exit_status: exit_status
    623     }
    624     |> add_line_no_options(line_no, source_file)
    625     |> add_column_if_missing(trigger, line_no, column, source_file)
    626     |> add_check_and_category(check, issue_category)
    627   end
    628 
    629   defp add_check_and_category(issue, check, issue_category) do
    630     %Issue{
    631       issue
    632       | check: check,
    633         category: issue_category
    634     }
    635   end
    636 
    637   defp add_column_if_missing(issue, trigger, line_no, column, source_file) do
    638     if trigger && line_no && !column do
    639       %Issue{
    640         issue
    641         | column: SourceFile.column(source_file, line_no, trigger)
    642       }
    643     else
    644       issue
    645     end
    646   end
    647 
    648   defp add_line_no_options(issue, line_no, source_file) do
    649     if line_no do
    650       {_def, scope} = scope_for(source_file, line: line_no)
    651 
    652       %Issue{
    653         issue
    654         | priority: issue.priority + priority_for(source_file, scope),
    655           scope: scope
    656       }
    657     else
    658       issue
    659     end
    660   end
    661 
    662   # Returns the scope for the given line as a tuple consisting of the call to
    663   # define the scope (`:defmodule`, `:def`, `:defp` or `:defmacro`) and the
    664   # name of the scope.
    665   #
    666   # Examples:
    667   #
    668   #     {:defmodule, "Foo.Bar"}
    669   #     {:def, "Foo.Bar.baz"}
    670   #
    671   @doc false
    672   def scope_for(source_file, line: line_no) do
    673     source_file
    674     |> scope_list
    675     |> Enum.at(line_no - 1)
    676   end
    677 
    678   # Returns all scopes for the given source_file per line of source code as tuple
    679   # consisting of the call to define the scope
    680   # (`:defmodule`, `:def`, `:defp` or `:defmacro`) and the name of the scope.
    681   #
    682   # Examples:
    683   #
    684   #     [
    685   #       {:defmodule, "Foo.Bar"},
    686   #       {:def, "Foo.Bar.baz"},
    687   #       {:def, "Foo.Bar.baz"},
    688   #       {:def, "Foo.Bar.baz"},
    689   #       {:def, "Foo.Bar.baz"},
    690   #       {:defmodule, "Foo.Bar"}
    691   #     ]
    692   defp scope_list(%SourceFile{} = source_file) do
    693     case SourceFileScopes.get(source_file) do
    694       {:ok, value} ->
    695         value
    696 
    697       :notfound ->
    698         ast = SourceFile.ast(source_file)
    699         lines = SourceFile.lines(source_file)
    700         scope_info_list = Scope.scope_info_list(ast)
    701 
    702         result =
    703           Enum.map(lines, fn {line_no, _} ->
    704             Scope.name_from_scope_info_list(scope_info_list, line_no)
    705           end)
    706 
    707         SourceFileScopes.put(source_file, result)
    708 
    709         result
    710     end
    711   end
    712 
    713   defp priority_for(source_file, scope) do
    714     scope_prio_map = Priority.scope_priorities(source_file)
    715 
    716     scope_prio_map[scope] || 0
    717   end
    718 
    719   defp category_body(nil) do
    720     quote do
    721       name =
    722         __MODULE__
    723         |> Module.split()
    724         |> Enum.at(2)
    725 
    726       safe_name = name || :unknown
    727 
    728       safe_name
    729       |> to_string
    730       |> String.downcase()
    731       |> String.to_atom()
    732     end
    733   end
    734 
    735   defp category_body(value), do: value
    736 
    737   @doc "Converts a given category to an exit status"
    738   def to_exit_status(nil), do: 0
    739 
    740   def to_exit_status(atom) when is_atom(atom) do
    741     to_exit_status(@base_category_exit_status_map[atom])
    742   end
    743 
    744   def to_exit_status(value) when is_number(value), do: value
    745 
    746   @doc false
    747   def defined?(check)
    748 
    749   def defined?({atom, _params}), do: defined?(atom)
    750 
    751   def defined?(binary) when is_binary(binary) do
    752     binary |> String.to_atom() |> defined?()
    753   end
    754 
    755   def defined?(module) when is_atom(module) do
    756     case Code.ensure_compiled(module) do
    757       {:module, _} -> true
    758       {:error, _} -> false
    759     end
    760   end
    761 end