zf

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

config_file.ex (13338B)


      1 defmodule Credo.ConfigFile do
      2   @moduledoc """
      3   `ConfigFile` structs represent all loaded and merged config files in a run.
      4   """
      5 
      6   @config_filename ".credo.exs"
      7   @default_config_name "default"
      8   @origin_user :file
      9 
     10   @default_glob "**/*.{ex,exs}"
     11   @default_files_included [@default_glob]
     12   @default_files_excluded []
     13   @default_parse_timeout 5000
     14   @default_strict false
     15   @default_color true
     16   @valid_checks_keys ~w(enabled disabled extra)a
     17 
     18   alias Credo.Execution
     19 
     20   defstruct origin: nil,
     21             filename: nil,
     22             config_name_found?: nil,
     23             files: nil,
     24             color: true,
     25             checks: nil,
     26             requires: [],
     27             plugins: [],
     28             parse_timeout: nil,
     29             strict: false
     30 
     31   @doc """
     32   Returns Execution struct representing a consolidated Execution for all `.credo.exs`
     33   files in `relevant_directories/1` merged into the default configuration.
     34 
     35   - `config_name`: name of the configuration to load
     36   - `safe`: if +true+, the config files are loaded using static analysis rather
     37             than `Code.eval_string/1`
     38   """
     39   def read_or_default(exec, dir, config_name \\ nil, safe \\ false) do
     40     dir
     41     |> relevant_config_files
     42     |> combine_configs(exec, dir, config_name, safe)
     43   end
     44 
     45   @doc """
     46   Returns the provided config_file merged into the default configuration.
     47 
     48   - `config_file`: full path to the custom configuration file
     49   - `config_name`: name of the configuration to load
     50   - `safe`: if +true+, the config files are loaded using static analysis rather
     51             than `Code.eval_string/1`
     52   """
     53   def read_from_file_path(exec, dir, config_filename, config_name \\ nil, safe \\ false) do
     54     if File.exists?(config_filename) do
     55       combine_configs([config_filename], exec, dir, config_name, safe)
     56     else
     57       {:error, {:notfound, "Given config file does not exist: #{config_filename}"}}
     58     end
     59   end
     60 
     61   defp combine_configs(files, exec, dir, config_name, safe) do
     62     config_files =
     63       files
     64       |> Enum.filter(&File.exists?/1)
     65       |> Enum.map(&{@origin_user, &1, File.read!(&1)})
     66 
     67     exec = Enum.reduce(config_files, exec, &Execution.append_config_file(&2, &1))
     68 
     69     Execution.get_config_files(exec)
     70     |> Enum.map(&from_exs(exec, dir, config_name || @default_config_name, &1, safe))
     71     |> ensure_any_config_found(config_name)
     72     |> merge()
     73     |> map_ok_files()
     74     |> ensure_values_present()
     75   end
     76 
     77   defp ensure_any_config_found(list, config_name) do
     78     config_not_found =
     79       Enum.all?(list, fn
     80         {:ok, %__MODULE__{config_name_found?: false}} -> true
     81         _ -> false
     82       end)
     83 
     84     if config_not_found do
     85       filenames_as_list =
     86         list
     87         |> Enum.map(fn
     88           {:ok, %__MODULE__{origin: :file, filename: filename}} -> "  * #{filename}\n"
     89           _ -> nil
     90         end)
     91         |> Enum.reject(&is_nil/1)
     92 
     93       message =
     94         case filenames_as_list do
     95           [] ->
     96             "Given config name #{inspect(config_name)} does not exist."
     97 
     98           filenames_as_list ->
     99             """
    100             Given config name #{inspect(config_name)} does not exist in any config file:
    101 
    102             #{filenames_as_list}
    103             """
    104         end
    105         |> String.trim()
    106 
    107       {:error, {:config_name_not_found, message}}
    108     else
    109       list
    110     end
    111   end
    112 
    113   defp relevant_config_files(dir) do
    114     dir
    115     |> relevant_directories
    116     |> add_config_files
    117   end
    118 
    119   @doc """
    120   Returns all parent directories of the given `dir` as well as each `./config`
    121   sub-directory.
    122   """
    123   def relevant_directories(dir) do
    124     dir
    125     |> Path.expand()
    126     |> Path.split()
    127     |> Enum.reverse()
    128     |> get_dir_paths
    129     |> add_config_dirs
    130   end
    131 
    132   defp ensure_values_present({:ok, config}) do
    133     # TODO: config.check_for_updates is deprecated, but should not lead to a validation error
    134     config = %__MODULE__{
    135       origin: config.origin,
    136       filename: config.filename,
    137       config_name_found?: config.config_name_found?,
    138       checks: config.checks,
    139       color: merge_boolean(@default_color, config.color),
    140       files: %{
    141         included: merge_files_default(@default_files_included, config.files.included),
    142         excluded: merge_files_default(@default_files_excluded, config.files.excluded)
    143       },
    144       parse_timeout: merge_parse_timeout(@default_parse_timeout, config.parse_timeout),
    145       plugins: config.plugins || [],
    146       requires: config.requires || [],
    147       strict: merge_boolean(@default_strict, config.strict)
    148     }
    149 
    150     {:ok, config}
    151   end
    152 
    153   defp ensure_values_present(error), do: error
    154 
    155   defp get_dir_paths(dirs), do: do_get_dir_paths(dirs, [])
    156 
    157   defp do_get_dir_paths(dirs, acc) when length(dirs) < 2, do: acc
    158 
    159   defp do_get_dir_paths([dir | tail], acc) do
    160     expanded_path =
    161       tail
    162       |> Enum.reverse()
    163       |> Path.join()
    164       |> Path.join(dir)
    165 
    166     do_get_dir_paths(tail, [expanded_path | acc])
    167   end
    168 
    169   defp add_config_dirs(paths) do
    170     Enum.flat_map(paths, fn path -> [path, Path.join(path, "config")] end)
    171   end
    172 
    173   defp add_config_files(paths) do
    174     for path <- paths, do: Path.join(path, @config_filename)
    175   end
    176 
    177   defp from_exs(exec, dir, config_name, {origin, filename, exs_string}, safe) do
    178     case Credo.ExsLoader.parse(exs_string, filename, exec, safe) do
    179       {:ok, data} ->
    180         from_data(data, dir, filename, origin, config_name)
    181 
    182       {:error, {line_no, description, trigger}} ->
    183         {:error, {:badconfig, filename, line_no, description, trigger}}
    184 
    185       {:error, reason} ->
    186         {:error, {:badconfig, filename, reason}}
    187     end
    188   end
    189 
    190   defp from_data(data, dir, filename, origin, config_name) do
    191     data =
    192       data[:configs]
    193       |> List.wrap()
    194       |> Enum.find(&(&1[:name] == config_name))
    195 
    196     config_name_found? = not is_nil(data)
    197 
    198     config_file = %__MODULE__{
    199       origin: origin,
    200       filename: filename,
    201       config_name_found?: config_name_found?,
    202       checks: checks_from_data(data, filename),
    203       color: data[:color],
    204       files: files_from_data(data, dir),
    205       parse_timeout: data[:parse_timeout],
    206       plugins: data[:plugins] || [],
    207       requires: data[:requires] || [],
    208       strict: data[:strict]
    209     }
    210 
    211     {:ok, config_file}
    212   end
    213 
    214   defp files_from_data(data, dir) do
    215     case data[:files] do
    216       nil ->
    217         nil
    218 
    219       %{} = files ->
    220         included_files = files[:included] || dir
    221 
    222         included_dir =
    223           included_files
    224           |> List.wrap()
    225           |> Enum.map(&join_default_files_if_directory/1)
    226 
    227         %{
    228           included: included_dir,
    229           excluded: files[:excluded] || @default_files_excluded
    230         }
    231     end
    232   end
    233 
    234   defp checks_from_data(data, filename) do
    235     case data[:checks] do
    236       checks when is_list(checks) ->
    237         checks
    238 
    239       %{} = checks ->
    240         do_warn_if_check_params_invalid(checks, filename)
    241 
    242         checks
    243 
    244       _ ->
    245         []
    246     end
    247   end
    248 
    249   defp do_warn_if_check_params_invalid(checks, filename) do
    250     Enum.each(checks, fn
    251       {checks_key, _name} when checks_key not in @valid_checks_keys ->
    252         candidate = find_best_match(@valid_checks_keys, checks_key)
    253         warning = warning_message_for(filename, checks_key, candidate)
    254 
    255         Credo.CLI.Output.UI.warn([:red, warning])
    256 
    257       _ ->
    258         nil
    259     end)
    260   end
    261 
    262   defp warning_message_for(filename, checks_key, candidate) do
    263     if candidate do
    264       "** (config) #{filename}: config field `:checks` contains unknown key `#{inspect(checks_key)}`. Did you mean `#{inspect(candidate)}`?"
    265     else
    266       "** (config) #{filename}: config field `:checks` contains unknown key `#{inspect(checks_key)}`."
    267     end
    268   end
    269 
    270   defp find_best_match(candidates, given, threshold \\ 0.8) do
    271     given_string = to_string(given)
    272 
    273     {jaro_distance, candidate} =
    274       candidates
    275       |> Enum.map(fn candidate_name ->
    276         distance = String.jaro_distance(given_string, to_string(candidate_name))
    277         {distance, candidate_name}
    278       end)
    279       |> Enum.sort()
    280       |> List.last()
    281 
    282     if jaro_distance > threshold do
    283       candidate
    284     end
    285   end
    286 
    287   @doc """
    288   Merges the given structs from left to right, meaning that later entries
    289   overwrites earlier ones.
    290 
    291       merge(base, other)
    292 
    293   Any options in `other` will overwrite those in `base`.
    294 
    295   The `files:` field is merged, meaning that you can define `included` and/or
    296   `excluded` and only override the given one.
    297 
    298   The `checks:` field is merged.
    299   """
    300   def merge(list) when is_list(list) do
    301     base = List.first(list)
    302     tail = List.delete_at(list, 0)
    303 
    304     merge(tail, base)
    305   end
    306 
    307   # bubble up errors from parsing the config so we can deal with them at the top-level
    308   def merge({:error, _} = error), do: error
    309 
    310   def merge([], config), do: config
    311 
    312   def merge([other | tail], base) do
    313     new_base = merge(base, other)
    314 
    315     merge(tail, new_base)
    316   end
    317 
    318   # bubble up errors from parsing the config so we can deal with them at the top-level
    319   def merge({:error, _} = a, _), do: a
    320   def merge(_, {:error, _} = a), do: a
    321 
    322   def merge({:ok, base}, {:ok, other}) do
    323     config_file = %__MODULE__{
    324       checks: merge_checks(base, other),
    325       color: merge_boolean(base.color, other.color),
    326       files: merge_files(base, other),
    327       parse_timeout: merge_parse_timeout(base.parse_timeout, other.parse_timeout),
    328       plugins: base.plugins ++ other.plugins,
    329       requires: base.requires ++ other.requires,
    330       strict: merge_boolean(base.strict, other.strict)
    331     }
    332 
    333     {:ok, config_file}
    334   end
    335 
    336   defp merge_boolean(base, other)
    337 
    338   defp merge_boolean(_base, true), do: true
    339   defp merge_boolean(_base, false), do: false
    340   defp merge_boolean(base, _), do: base
    341 
    342   defp merge_files_default(_base, [_head | _tail] = non_empty_list), do: non_empty_list
    343   defp merge_files_default(base, _), do: base
    344 
    345   defp merge_parse_timeout(_base, timeout) when is_integer(timeout), do: timeout
    346   defp merge_parse_timeout(base, _), do: base
    347 
    348   def merge_checks(%__MODULE__{checks: checks_list_base}, %__MODULE__{checks: checks_list_other})
    349       when is_list(checks_list_base) and is_list(checks_list_other) do
    350     base = %__MODULE__{checks: %{enabled: checks_list_base}}
    351     other = %__MODULE__{checks: %{extra: checks_list_other}}
    352 
    353     merge_checks(base, other)
    354   end
    355 
    356   def merge_checks(%__MODULE__{checks: checks_base}, %__MODULE__{
    357         checks: %{extra: _} = checks_map_other
    358       })
    359       when is_list(checks_base) do
    360     base = %__MODULE__{checks: %{enabled: checks_base}}
    361     other = %__MODULE__{checks: checks_map_other}
    362 
    363     merge_checks(base, other)
    364   end
    365 
    366   def merge_checks(%__MODULE__{checks: %{enabled: checks_list_base}}, %__MODULE__{
    367         checks: checks_other
    368       })
    369       when is_list(checks_list_base) and is_list(checks_other) do
    370     base = %__MODULE__{checks: %{enabled: checks_list_base}}
    371     other = %__MODULE__{checks: %{extra: checks_other}}
    372 
    373     merge_checks(base, other)
    374   end
    375 
    376   def merge_checks(%__MODULE__{checks: _checks_base}, %__MODULE__{
    377         checks: %{enabled: checks_other_enabled} = checks_other
    378       })
    379       when is_list(checks_other_enabled) do
    380     disabled = disable_check_tuples(checks_other[:disabled])
    381 
    382     %{
    383       enabled: checks_other_enabled |> normalize_check_tuples() |> Keyword.merge(disabled),
    384       disabled: checks_other[:disabled] || []
    385     }
    386   end
    387 
    388   def merge_checks(%__MODULE__{checks: %{enabled: checks_base}}, %__MODULE__{
    389         checks: %{} = checks_other
    390       })
    391       when is_list(checks_base) do
    392     base = normalize_check_tuples(checks_base)
    393     other = normalize_check_tuples(checks_other[:extra])
    394     disabled = disable_check_tuples(checks_other[:disabled])
    395 
    396     %{
    397       enabled: base |> Keyword.merge(other) |> Keyword.merge(disabled),
    398       disabled: checks_other[:disabled] || []
    399     }
    400   end
    401 
    402   # this def catches all the cases where no valid key was found in `checks_map_other`
    403   def merge_checks(%__MODULE__{checks: %{enabled: checks_base}}, %__MODULE__{
    404         checks: %{}
    405       })
    406       when is_list(checks_base) do
    407     base = %__MODULE__{checks: %{enabled: checks_base}}
    408     other = %__MODULE__{checks: []}
    409 
    410     merge_checks(base, other)
    411   end
    412 
    413   #
    414 
    415   def merge_files(%__MODULE__{files: files_base}, %__MODULE__{files: files_other}) do
    416     %{
    417       included: files_other[:included] || files_base[:included],
    418       excluded: files_other[:excluded] || files_base[:excluded]
    419     }
    420   end
    421 
    422   defp normalize_check_tuples(nil), do: []
    423 
    424   defp normalize_check_tuples(list) when is_list(list) do
    425     Enum.map(list, &normalize_check_tuple/1)
    426   end
    427 
    428   defp normalize_check_tuple({name}), do: {name, []}
    429   defp normalize_check_tuple(tuple), do: tuple
    430 
    431   defp disable_check_tuples(nil), do: []
    432 
    433   defp disable_check_tuples(list) when is_list(list) do
    434     Enum.map(list, &disable_check_tuple/1)
    435   end
    436 
    437   defp disable_check_tuple({name}), do: {name, false}
    438   defp disable_check_tuple({name, _params}), do: {name, false}
    439 
    440   defp join_default_files_if_directory(dir) do
    441     if File.dir?(dir) do
    442       Path.join(dir, @default_files_included)
    443     else
    444       dir
    445     end
    446   end
    447 
    448   defp map_ok_files({:error, _} = error) do
    449     error
    450   end
    451 
    452   defp map_ok_files({:ok, %__MODULE__{files: files} = config}) do
    453     files = %{
    454       included:
    455         files[:included]
    456         |> List.wrap()
    457         |> Enum.uniq(),
    458       excluded:
    459         files[:excluded]
    460         |> List.wrap()
    461         |> Enum.uniq()
    462     }
    463 
    464     {:ok, %__MODULE__{config | files: files}}
    465   end
    466 end