zf

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

pipe_chain_start.ex (7608B)


      1 defmodule Credo.Check.Refactor.PipeChainStart do
      2   use Credo.Check,
      3     tags: [:controversial],
      4     param_defaults: [
      5       excluded_argument_types: [],
      6       excluded_functions: []
      7     ],
      8     explanations: [
      9       check: """
     10       Pipes (`|>`) can become more readable by starting with a "raw" value.
     11 
     12       So while this is easily comprehendable:
     13 
     14           list
     15           |> Enum.take(5)
     16           |> Enum.shuffle
     17           |> pick_winner()
     18 
     19       This might be harder to read:
     20 
     21           Enum.take(list, 5)
     22           |> Enum.shuffle
     23           |> pick_winner()
     24 
     25       As always: This is just a suggestion. Check the configuration options for
     26       tweaking or disabling this check.
     27       """,
     28       params: [
     29         excluded_functions: "All functions listed will be ignored.",
     30         excluded_argument_types: "All pipes with argument types listed will be ignored."
     31       ]
     32     ]
     33 
     34   @elixir_custom_operators [
     35     :<-,
     36     :|||,
     37     :&&&,
     38     :<<<,
     39     :>>>,
     40     :<<~,
     41     :~>>,
     42     :<~,
     43     :~>,
     44     :<~>,
     45     :<|>,
     46     :^^^,
     47     :~~~,
     48     :"..//"
     49   ]
     50 
     51   @doc false
     52   @impl true
     53   def run(%SourceFile{} = source_file, params) do
     54     issue_meta = IssueMeta.for(source_file, params)
     55 
     56     excluded_functions = Params.get(params, :excluded_functions, __MODULE__)
     57 
     58     excluded_argument_types = Params.get(params, :excluded_argument_types, __MODULE__)
     59 
     60     Credo.Code.prewalk(
     61       source_file,
     62       &traverse(&1, &2, issue_meta, excluded_functions, excluded_argument_types)
     63     )
     64   end
     65 
     66   # TODO: consider for experimental check front-loader (ast)
     67   defp traverse(
     68          {:|>, _, [{:|>, _, _} | _]} = ast,
     69          issues,
     70          _issue_meta,
     71          _excluded_functions,
     72          _excluded_argument_types
     73        ) do
     74     {ast, issues}
     75   end
     76 
     77   defp traverse(
     78          {:|>, meta, [lhs | _rhs]} = ast,
     79          issues,
     80          issue_meta,
     81          excluded_functions,
     82          excluded_argument_types
     83        ) do
     84     if valid_chain_start?(lhs, excluded_functions, excluded_argument_types) do
     85       {ast, issues}
     86     else
     87       {ast, issues ++ [issue_for(issue_meta, meta[:line], "TODO")]}
     88     end
     89   end
     90 
     91   defp traverse(
     92          ast,
     93          issues,
     94          _issue_meta,
     95          _excluded_functions,
     96          _excluded_argument_types
     97        ) do
     98     {ast, issues}
     99   end
    100 
    101   defp valid_chain_start?(
    102          {:__block__, _, [single_ast_node]},
    103          excluded_functions,
    104          excluded_argument_types
    105        ) do
    106     valid_chain_start?(
    107       single_ast_node,
    108       excluded_functions,
    109       excluded_argument_types
    110     )
    111   end
    112 
    113   for atom <- [
    114         :%,
    115         :%{},
    116         :..,
    117         :<<>>,
    118         :@,
    119         :__aliases__,
    120         :unquote,
    121         :{},
    122         :&,
    123         :<>,
    124         :++,
    125         :--,
    126         :&&,
    127         :||,
    128         :+,
    129         :-,
    130         :*,
    131         :/,
    132         :>,
    133         :>=,
    134         :<,
    135         :<=,
    136         :==,
    137         :for,
    138         :with,
    139         :not,
    140         :and,
    141         :or
    142       ] do
    143     defp valid_chain_start?(
    144            {unquote(atom), _meta, _arguments},
    145            _excluded_functions,
    146            _excluded_argument_types
    147          ) do
    148       true
    149     end
    150   end
    151 
    152   for operator <- @elixir_custom_operators do
    153     defp valid_chain_start?(
    154            {unquote(operator), _meta, _arguments},
    155            _excluded_functions,
    156            _excluded_argument_types
    157          ) do
    158       true
    159     end
    160   end
    161 
    162   # anonymous function
    163   defp valid_chain_start?(
    164          {:fn, _, [{:->, _, [_args, _body]}]},
    165          _excluded_functions,
    166          _excluded_argument_types
    167        ) do
    168     true
    169   end
    170 
    171   # function_call()
    172   defp valid_chain_start?(
    173          {atom, _, []},
    174          _excluded_functions,
    175          _excluded_argument_types
    176        )
    177        when is_atom(atom) do
    178     true
    179   end
    180 
    181   # function_call(with, args) and sigils
    182   defp valid_chain_start?(
    183          {atom, _, arguments} = ast,
    184          excluded_functions,
    185          excluded_argument_types
    186        )
    187        when is_atom(atom) and is_list(arguments) do
    188     sigil?(atom) ||
    189       valid_chain_start_function_call?(
    190         ast,
    191         excluded_functions,
    192         excluded_argument_types
    193       )
    194   end
    195 
    196   # map[:access]
    197   defp valid_chain_start?(
    198          {{:., _, [Access, :get]}, _, _},
    199          _excluded_functions,
    200          _excluded_argument_types
    201        ) do
    202     true
    203   end
    204 
    205   # Module.function_call()
    206   defp valid_chain_start?(
    207          {{:., _, _}, _, []},
    208          _excluded_functions,
    209          _excluded_argument_types
    210        ),
    211        do: true
    212 
    213   # Elixir <= 1.8.0
    214   # '__#{val}__' are compiled to String.to_charlist("__#{val}__")
    215   # we want to consider these charlists a valid pipe chain start
    216   defp valid_chain_start?(
    217          {{:., _, [String, :to_charlist]}, _, [{:<<>>, _, _}]},
    218          _excluded_functions,
    219          _excluded_argument_types
    220        ),
    221        do: true
    222 
    223   # Elixir >= 1.8.0
    224   # '__#{val}__' are compiled to String.to_charlist("__#{val}__")
    225   # we want to consider these charlists a valid pipe chain start
    226   defp valid_chain_start?(
    227          {{:., _, [List, :to_charlist]}, _, [[_ | _]]},
    228          _excluded_functions,
    229          _excluded_argument_types
    230        ),
    231        do: true
    232 
    233   # Module.function_call(with, parameters)
    234   defp valid_chain_start?(
    235          {{:., _, _}, _, _} = ast,
    236          excluded_functions,
    237          excluded_argument_types
    238        ) do
    239     valid_chain_start_function_call?(
    240       ast,
    241       excluded_functions,
    242       excluded_argument_types
    243     )
    244   end
    245 
    246   defp valid_chain_start?(_, _excluded_functions, _excluded_argument_types), do: true
    247 
    248   defp valid_chain_start_function_call?(
    249          {_atom, _, arguments} = ast,
    250          excluded_functions,
    251          excluded_argument_types
    252        ) do
    253     function_name = to_function_call_name(ast)
    254 
    255     found_argument_types =
    256       case arguments do
    257         [nil | _] -> [:atom]
    258         x -> x |> List.first() |> argument_type()
    259       end
    260 
    261     Enum.member?(excluded_functions, function_name) ||
    262       Enum.any?(
    263         found_argument_types,
    264         &Enum.member?(excluded_argument_types, &1)
    265       )
    266   end
    267 
    268   defp sigil?(atom) do
    269     atom
    270     |> to_string
    271     |> String.match?(~r/^sigil_[a-zA-Z]$/)
    272   end
    273 
    274   defp to_function_call_name({_, _, _} = ast) do
    275     {ast, [], []}
    276     |> Macro.to_string()
    277     |> String.replace(~r/\.?\(.*\)$/s, "")
    278   end
    279 
    280   @alphabet_wo_r ~w(a b c d e f g h i j k l m n o p q s t u v w x y z)
    281   @all_sigil_chars Enum.flat_map(@alphabet_wo_r, &[&1, String.upcase(&1)])
    282   @matchable_sigils Enum.map(@all_sigil_chars, &:"sigil_#{&1}")
    283 
    284   for sigil_atom <- @matchable_sigils do
    285     defp argument_type({unquote(sigil_atom), _, _}) do
    286       [unquote(sigil_atom)]
    287     end
    288   end
    289 
    290   defp argument_type({:sigil_r, _, _}), do: [:sigil_r, :regex]
    291   defp argument_type({:sigil_R, _, _}), do: [:sigil_R, :regex]
    292 
    293   defp argument_type({:fn, _, _}), do: [:fn]
    294   defp argument_type({:%{}, _, _}), do: [:map]
    295   defp argument_type({:{}, _, _}), do: [:tuple]
    296   defp argument_type(nil), do: []
    297 
    298   defp argument_type(v) when is_atom(v), do: [:atom]
    299   defp argument_type(v) when is_binary(v), do: [:binary]
    300   defp argument_type(v) when is_bitstring(v), do: [:bitstring]
    301   defp argument_type(v) when is_boolean(v), do: [:boolean]
    302 
    303   defp argument_type(v) when is_list(v) do
    304     if Keyword.keyword?(v) do
    305       [:keyword, :list]
    306     else
    307       [:list]
    308     end
    309   end
    310 
    311   defp argument_type(v) when is_number(v), do: [:number]
    312 
    313   defp argument_type(v), do: [:credo_type_error, v]
    314 
    315   defp issue_for(issue_meta, line_no, trigger) do
    316     format_issue(
    317       issue_meta,
    318       message: "Pipe chain should start with a raw value.",
    319       trigger: trigger,
    320       line_no: line_no
    321     )
    322   end
    323 end