zf

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

collector.ex (6110B)


      1 defmodule Credo.Check.Consistency.Collector do
      2   @moduledoc """
      3   A behavior for modules that walk through source files and
      4   identify consistency issues.
      5 
      6   When defining a consistency check, you would typically use
      7   this structure for the main module, responsible
      8   for formatting issue messages:
      9 
     10       defmodule Credo.Check.Consistency.SomeCheck do
     11         use Credo.Check, run_on_all: true
     12 
     13         @collector Credo.Check.Consistency.SomeCheck.Collector
     14 
     15         def run(source_files, exec, params) when is_list(source_files) do
     16           issue_formatter = &issues_for/3
     17 
     18           @collector.find_and_append_issues(source_files, exec, params, issue_formatter)
     19         end
     20 
     21         defp issues_for(expected, source_file, params) do
     22           issue_meta = IssueMeta.for(source_file, params)
     23           issue_locations =
     24             @collector.find_locations_not_matching(expected, source_file)
     25 
     26           Enum.map(issue_locations, fn(location) ->
     27             format_issue issue_meta, message: ... # write an issue message
     28           end)
     29         end
     30 
     31   The actual analysis would be performed by another module
     32   implementing the `Credo.Check.Consistency.Collector` behavior:
     33 
     34       defmodule Credo.Check.Consistency.SomeCheck.Collector do
     35         use Credo.Check.Consistency.Collector
     36 
     37         def collect_matches(source_file, params) do
     38           # ...
     39         end
     40 
     41         def find_locations_not_matching(expected, source_file) do
     42           # ...
     43         end
     44       end
     45 
     46   Read further for more information on `collect_matches/2`,
     47   `find_locations_not_matching/2`, and `issue_formatter`.
     48   """
     49 
     50   alias Credo.Execution.ExecutionIssues
     51   alias Credo.Issue
     52   alias Credo.SourceFile
     53 
     54   @doc """
     55   When you call `@collector.find_and_append_issues/4` inside the check module,
     56   the collector first counts the occurrences of different matches
     57   (e.g. :with_space and :without_space for a space around operators check)
     58   per each source file.
     59 
     60   `collect_matches/2` produces a map of matches as keys and their frequencies
     61   as values (e.g. %{with_space: 50, without_space: 40}).
     62 
     63   The maps for individual source files are then merged, producing a map
     64   that reflects frequency trends for the whole codebase.
     65   """
     66   @callback collect_matches(
     67               source_file :: SourceFile.t(),
     68               params :: Keyword.t()
     69             ) :: %{
     70               term => non_neg_integer
     71             }
     72 
     73   # Once the most frequent match is identified, the `Collector` looks up
     74   # source files that have other matches (e.g. both :with_space
     75   # and :without_space or just :without_space when :with_space is the
     76   # most frequent) and calls the `issue_formatter` function on them.
     77   #
     78   # An issue formatter produces a list of `Credo.Issue` structs
     79   # from the most frequent (expected) match, a source file
     80   # containing other matches, and check params
     81   # (the latter two are required to build an IssueMeta).
     82   @type issue_formatter :: (term, SourceFile.t(), Keyword.t() -> [Issue.t()])
     83 
     84   @doc """
     85   `issue_formatter` may call the `@collector.find_locations_not_matching/2`
     86   function to obtain additional metadata for each occurrence of
     87   an unexpected match in a given file.
     88 
     89   An example implementation that returns a list of line numbers on
     90   which unexpected occurrences were found:
     91 
     92       def find_locations_not_matching(expected, source_file) do
     93         traverse(source_file, fn(match, line_no, acc) ->
     94           if match != expected do
     95             acc ++ [line_no]
     96           else
     97             acc
     98           end
     99         end)
    100       end
    101 
    102       defp traverse(source_file, fun), do: ...
    103   """
    104   @callback find_locations_not_matching(
    105               expected :: term,
    106               source_file :: SourceFile.t()
    107             ) :: list(term)
    108 
    109   @optional_callbacks find_locations_not_matching: 2
    110 
    111   defmacro __using__(_opts) do
    112     quote do
    113       @behaviour Credo.Check.Consistency.Collector
    114 
    115       alias Credo.Check.Consistency.Collector
    116       alias Credo.Execution
    117       alias Credo.Issue
    118       alias Credo.SourceFile
    119 
    120       @spec find_and_append_issues(
    121               [SourceFile.t()],
    122               Execution.t(),
    123               Keyword.t(),
    124               Collector.issue_formatter()
    125             ) :: atom
    126       def find_and_append_issues(source_files, exec, params, issue_formatter)
    127           when is_list(source_files) and is_function(issue_formatter) do
    128         source_files
    129         |> Collector.find_issues(__MODULE__, params, issue_formatter)
    130         |> Enum.each(&Collector.append_issue_via_issue_service(&1, exec))
    131 
    132         :ok
    133       end
    134     end
    135   end
    136 
    137   def find_issues(source_files, collector, params, issue_formatter) do
    138     frequencies_per_source_file =
    139       source_files
    140       |> Enum.map(&Task.async(fn -> {&1, collector.collect_matches(&1, params)} end))
    141       |> Enum.map(&Task.await(&1, :infinity))
    142 
    143     frequencies = total_frequencies(frequencies_per_source_file)
    144 
    145     if map_size(frequencies) > 0 do
    146       most_frequent_match =
    147         case params[:force] do
    148           nil ->
    149             {most_frequent_match, _frequency} = Enum.max_by(frequencies, &elem(&1, 1))
    150 
    151             most_frequent_match
    152 
    153           value ->
    154             value
    155         end
    156 
    157       result =
    158         frequencies_per_source_file
    159         |> source_files_with_issues(most_frequent_match)
    160         |> Enum.map(&Task.async(fn -> issue_formatter.(most_frequent_match, &1, params) end))
    161         |> Enum.flat_map(&Task.await(&1, :infinity))
    162 
    163       result
    164     else
    165       []
    166     end
    167   end
    168 
    169   def append_issue_via_issue_service(%Issue{} = issue, exec) do
    170     ExecutionIssues.append(exec, issue)
    171   end
    172 
    173   defp source_files_with_issues(frequencies_per_file, most_frequent_match) do
    174     Enum.reduce(frequencies_per_file, [], fn {filename, stats}, acc ->
    175       unexpected_matches = Map.keys(stats) -- [most_frequent_match]
    176 
    177       if unexpected_matches != [] do
    178         [filename | acc]
    179       else
    180         acc
    181       end
    182     end)
    183   end
    184 
    185   defp total_frequencies(frequencies_per_file) do
    186     Enum.reduce(frequencies_per_file, %{}, fn {_, file_stats}, stats ->
    187       Map.merge(stats, file_stats, fn _k, f1, f2 -> f1 + f2 end)
    188     end)
    189   end
    190 end