zf

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

sources.ex (5597B)


      1 defmodule Credo.Sources do
      2   @moduledoc """
      3   This module is used to find and read all source files for analysis.
      4   """
      5 
      6   alias Credo.Execution
      7   alias Credo.SourceFile
      8 
      9   @default_sources_glob ~w(** *.{ex,exs})
     10   @stdin_filename "stdin"
     11 
     12   @doc """
     13   Finds sources for a given `Credo.Execution`.
     14 
     15   Through the `files` key, configs may contain a list of `included` and `excluded`
     16   patterns. For `included`, patterns can be file paths, directory paths and globs.
     17   For `excluded`, patterns can also be specified as regular expressions.
     18 
     19       iex> Sources.find(%Credo.Execution{files: %{excluded: ["not_me.ex"], included: ["*.ex"]}})
     20 
     21       iex> Sources.find(%Credo.Execution{files: %{excluded: [~r/messy/], included: ["lib/mix", "root.ex"]}})
     22   """
     23   def find(exec)
     24 
     25   def find(%Execution{read_from_stdin: true, files: %{included: [filename]}}) do
     26     filename
     27     |> source_file_from_stdin()
     28     |> List.wrap()
     29   end
     30 
     31   def find(%Execution{read_from_stdin: true}) do
     32     @stdin_filename
     33     |> source_file_from_stdin()
     34     |> List.wrap()
     35   end
     36 
     37   def find(%Execution{files: files, parse_timeout: parse_timeout} = exec) do
     38     parse_timeout =
     39       if is_nil(parse_timeout) do
     40         :infinity
     41       else
     42         parse_timeout
     43       end
     44 
     45     working_dir = Execution.working_dir(exec)
     46 
     47     included_patterns = convert_globs_to_local_paths(working_dir, files.included)
     48     excluded_patterns = convert_globs_to_local_paths(working_dir, files.excluded)
     49 
     50     MapSet.new()
     51     |> include(included_patterns)
     52     |> exclude(excluded_patterns)
     53     |> Enum.sort()
     54     |> Enum.take(max_file_count())
     55     |> read_files(parse_timeout)
     56   end
     57 
     58   def find(nil) do
     59     []
     60   end
     61 
     62   def find(paths) when is_list(paths) do
     63     Enum.flat_map(paths, &find/1)
     64   end
     65 
     66   def find(path) when is_binary(path) do
     67     recurse_path(path)
     68   end
     69 
     70   @doc """
     71   Finds sources in a given `directory` using a list of `included` and `excluded`
     72   patterns. For `included`, patterns can be file paths, directory paths and globs.
     73   For `excluded`, patterns can also be specified as regular expressions.
     74 
     75       iex> Sources.find_in_dir("/home/rrrene/elixir", ["*.ex"], ["not_me.ex"])
     76 
     77       iex> Sources.find_in_dir("/home/rrrene/elixir", ["*.ex"], [~r/messy/])
     78   """
     79   def find_in_dir(working_dir, included, excluded)
     80 
     81   def find_in_dir(working_dir, [], excluded),
     82     do: find_in_dir(working_dir, [Path.join(@default_sources_glob)], excluded)
     83 
     84   def find_in_dir(working_dir, included, excluded) do
     85     included_patterns = convert_globs_to_local_paths(working_dir, included)
     86     excluded_patterns = convert_globs_to_local_paths(working_dir, excluded)
     87 
     88     MapSet.new()
     89     |> include(included_patterns)
     90     |> exclude(excluded_patterns)
     91     |> Enum.sort()
     92     |> Enum.take(max_file_count())
     93   end
     94 
     95   defp convert_globs_to_local_paths(working_dir, patterns) do
     96     patterns
     97     |> List.wrap()
     98     |> Enum.map(fn
     99       pattern when is_binary(pattern) -> Path.expand(pattern, working_dir)
    100       pattern -> pattern
    101     end)
    102   end
    103 
    104   defp max_file_count do
    105     max_files = System.get_env("MAX_FILES")
    106 
    107     if max_files do
    108       String.to_integer(max_files)
    109     else
    110       1_000_000
    111     end
    112   end
    113 
    114   defp include(files, []), do: files
    115 
    116   defp include(files, [path | remaining_paths]) do
    117     include_paths =
    118       path
    119       |> recurse_path()
    120       |> Enum.into(MapSet.new())
    121 
    122     files
    123     |> MapSet.union(include_paths)
    124     |> include(remaining_paths)
    125   end
    126 
    127   defp exclude(files, []), do: files
    128 
    129   defp exclude(files, [pattern | remaining_patterns]) when is_list(files) do
    130     files
    131     |> Enum.into(MapSet.new())
    132     |> exclude([pattern | remaining_patterns])
    133   end
    134 
    135   defp exclude(files, [pattern | remaining_patterns]) when is_binary(pattern) do
    136     exclude_paths =
    137       pattern
    138       |> recurse_path
    139       |> Enum.into(MapSet.new())
    140 
    141     files
    142     |> MapSet.difference(exclude_paths)
    143     |> exclude(remaining_patterns)
    144   end
    145 
    146   defp exclude(files, [pattern | remaining_patterns]) do
    147     files
    148     |> Enum.reject(&String.match?(&1, pattern))
    149     |> exclude(remaining_patterns)
    150   end
    151 
    152   defp recurse_path(path) do
    153     paths =
    154       cond do
    155         File.regular?(path) ->
    156           [path]
    157 
    158         File.dir?(path) ->
    159           [path | @default_sources_glob]
    160           |> Path.join()
    161           |> Path.wildcard()
    162 
    163         true ->
    164           path
    165           |> Path.wildcard()
    166           |> Enum.flat_map(&recurse_path/1)
    167       end
    168 
    169     Enum.map(paths, &Path.expand/1)
    170   end
    171 
    172   defp read_files(filenames, parse_timeout) do
    173     tasks = Enum.map(filenames, &Task.async(fn -> to_source_file(&1) end))
    174 
    175     task_dictionary =
    176       tasks
    177       |> Enum.zip(filenames)
    178       |> Enum.into(%{})
    179 
    180     tasks_with_results = Task.yield_many(tasks, parse_timeout)
    181 
    182     results =
    183       Enum.map(tasks_with_results, fn {task, res} ->
    184         # Shutdown the tasks that did not reply nor exit
    185         {task, res || Task.shutdown(task, :brutal_kill)}
    186       end)
    187 
    188     Enum.map(results, fn
    189       {_task, {:ok, value}} -> value
    190       {task, nil} -> SourceFile.timed_out(task_dictionary[task])
    191     end)
    192   end
    193 
    194   defp to_source_file(filename) do
    195     filename
    196     |> File.read!()
    197     |> SourceFile.parse(filename)
    198   end
    199 
    200   defp source_file_from_stdin(filename) do
    201     SourceFile.parse(read_from_stdin!(), filename)
    202   end
    203 
    204   defp read_from_stdin! do
    205     {:ok, source} = read_from_stdin()
    206     source
    207   end
    208 
    209   defp read_from_stdin(source \\ "") do
    210     case IO.read(:stdio, :line) do
    211       {:error, reason} ->
    212         {:error, reason}
    213 
    214       :eof ->
    215         {:ok, source}
    216 
    217       data ->
    218         source = source <> data
    219         read_from_stdin(source)
    220     end
    221   end
    222 end