zf

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

interpolation_helper.ex (7409B)


      1 defmodule Credo.Code.InterpolationHelper do
      2   @moduledoc false
      3 
      4   alias Credo.Code.Token
      5 
      6   @doc false
      7   def replace_interpolations(source, char \\ " ", filename \\ "nofilename")
      8       when is_binary(source) do
      9     positions = interpolation_positions(source, filename)
     10     lines = String.split(source, "\n")
     11 
     12     positions
     13     |> Enum.reverse()
     14     |> Enum.reduce(lines, &replace_line(&1, &2, char))
     15     |> Enum.join("\n")
     16   end
     17 
     18   defp replace_line({line_no, col_start, line_no, col_end}, lines, char) do
     19     List.update_at(
     20       lines,
     21       line_no - 1,
     22       &replace_line(&1, col_start, col_end, char)
     23     )
     24   end
     25 
     26   defp replace_line(position, lines, char) do
     27     {line_no_start, col_start, line_no_end, col_end} = position
     28 
     29     Enum.reduce(line_no_start..line_no_end, lines, fn
     30       line_no, memo
     31       when line_no == line_no_start ->
     32         List.update_at(
     33           memo,
     34           line_no - 1,
     35           &replace_line(&1, col_start, String.length(&1) + 1, char)
     36         )
     37 
     38       line_no, memo
     39       when line_no == line_no_end ->
     40         List.update_at(
     41           memo,
     42           line_no - 1,
     43           &replace_line(&1, 1, col_end, char)
     44         )
     45 
     46       line_no, memo
     47       when line_no < line_no_end ->
     48         List.update_at(
     49           memo,
     50           line_no - 1,
     51           &replace_line(&1, 1, String.length(&1) + 1, char)
     52         )
     53     end)
     54   end
     55 
     56   defp replace_line(line, col_start, col_end, char) do
     57     length = max(col_end - col_start, 0)
     58     line = String.to_charlist(line)
     59     part1 = Enum.slice(line, 0, col_start - 1)
     60     part2 = String.to_charlist(String.duplicate(char, length))
     61     part3 = Enum.slice(line, (col_end - 1)..-1)
     62     List.to_string(part1 ++ part2 ++ part3)
     63   end
     64 
     65   @doc false
     66   def interpolation_positions(source, filename \\ "nofilename") do
     67     source
     68     |> Credo.Code.to_tokens(filename)
     69     |> Enum.flat_map(&map_interpolations(&1, source))
     70     |> Enum.reject(&is_nil/1)
     71   end
     72 
     73   #
     74   # Elixir >= 1.13.0
     75   #
     76   defp map_interpolations(
     77          {:bin_heredoc, {_line_no, _col_start, _}, _, _list} = token,
     78          source
     79        ) do
     80     handle_heredoc(token, source)
     81   end
     82 
     83   #
     84   # Elixir >= 1.11.0
     85   #
     86   defp map_interpolations(
     87          {:sigil, {_line_no, _col_start, nil}, _, list, _empty_list, nil, _another_binary} =
     88            token,
     89          source
     90        ) do
     91     handle_atom_string_or_sigil(token, list, source)
     92   end
     93 
     94   #
     95   # Elixir >= 1.6.0
     96   #
     97   defp map_interpolations(
     98          {:sigil, {_line_no, _col_start, nil}, _, list, _, _sigil_start_char} = token,
     99          source
    100        ) do
    101     handle_atom_string_or_sigil(token, list, source)
    102   end
    103 
    104   defp map_interpolations(
    105          {:bin_heredoc, {_line_no, _col_start, _}, _list} = token,
    106          source
    107        ) do
    108     handle_heredoc(token, source)
    109   end
    110 
    111   defp map_interpolations(
    112          {:bin_string, {_line_no, _col_start, _}, list} = token,
    113          source
    114        ) do
    115     handle_atom_string_or_sigil(token, list, source)
    116   end
    117 
    118   defp map_interpolations(
    119          {:kw_identifier_unsafe, {_line_no, _col_start, _}, list} = token,
    120          source
    121        ) do
    122     handle_atom_string_or_sigil(token, list, source)
    123   end
    124 
    125   defp map_interpolations(
    126          {:atom_unsafe, {_line_no, _col_start, _}, list} = token,
    127          source
    128        ) do
    129     handle_atom_string_or_sigil(token, list, source)
    130   end
    131 
    132   defp map_interpolations(_token, _source) do
    133     []
    134   end
    135 
    136   defp handle_atom_string_or_sigil(_token, list, source) do
    137     find_interpolations(list, source)
    138   end
    139 
    140   defp handle_heredoc({_atom, {line_no, _, _}, list}, source) do
    141     first_line_in_heredoc = get_line(source, line_no + 1)
    142 
    143     # TODO: this seems to be wrong. the closing """ determines the
    144     #       indentation, not the first line of the heredoc.
    145     padding_in_first_line = determine_heredoc_padding_at_start_of_line(first_line_in_heredoc)
    146 
    147     list
    148     |> find_interpolations(source)
    149     |> Enum.reject(&is_nil/1)
    150     |> add_to_col_start_and_end(padding_in_first_line)
    151   end
    152 
    153   # Elixir 1.13+
    154   defp handle_heredoc({atom, {line_no, col_start, col_end}, _, list}, source) do
    155     handle_heredoc({atom, {line_no, col_start, col_end}, list}, source)
    156   end
    157 
    158   defp find_interpolations(list, source) when is_list(list) do
    159     Enum.map(list, &find_interpolations(&1, source))
    160   end
    161 
    162   # Elixir < 1.9.0
    163   #
    164   defp find_interpolations(
    165          {{line_no, col_start, nil}, {line_no_end, col_end, nil}, list} = _token,
    166          _source
    167        )
    168        when is_list(list) do
    169     {line_no, col_start, line_no_end, col_end + 1}
    170   end
    171 
    172   # {{1, 25, 32}, [{:identifier, {1, 27, 31}, :name}]}
    173   defp find_interpolations({{_line_no, _col_start2, _}, _list} = token, source) do
    174     {line_no, col_start, line_no_end, col_end} = Token.position(token)
    175 
    176     col_end =
    177       if line_no_end > line_no && col_end == 1 do
    178         # This means we encountered :eol and jumped in the next line.
    179         # We need to add the closing `}`.
    180         col_end + 1
    181       else
    182         col_end
    183       end
    184 
    185     line = get_line(source, line_no_end)
    186 
    187     # `col_end - 1` to account for the closing `}`
    188     rest_of_line = get_rest_of_line(line, col_end - 1)
    189 
    190     # IO.inspect(rest_of_line, label: "rest_of_line")
    191 
    192     padding = determine_padding_at_start_of_line(rest_of_line, ~r/^\s*\}/)
    193 
    194     # -1 to remove the accounted-for `}`
    195     padding = max(padding - 1, 0)
    196 
    197     # IO.inspect(padding, label: "padding")
    198 
    199     {line_no, col_start, line_no_end, col_end + padding}
    200   end
    201 
    202   # Elixir >= 1.9.0
    203   #
    204   # {{1, 25, nil}, {1, 31, nil}, [{:identifier, {1, 27, nil}, :name}]}
    205   defp find_interpolations(
    206          {{_line_no, _col_start, nil}, {_line_no2, _col_start2, nil}, _list} = token,
    207          source
    208        ) do
    209     {line_no, col_start, line_no_end, col_end} = Token.position(token)
    210 
    211     col_end =
    212       if line_no_end > line_no && col_end == 1 do
    213         # This means we encountered :eol and jumped in the next line.
    214         # We need to add the closing `}`.
    215         col_end + 1
    216       else
    217         col_end
    218       end
    219 
    220     line = get_line(source, line_no_end)
    221 
    222     # `col_end - 1` to account for the closing `}`
    223     rest_of_line = get_rest_of_line(line, col_end - 1)
    224 
    225     # IO.inspect(rest_of_line, label: "rest_of_line")
    226 
    227     padding = determine_padding_at_start_of_line(rest_of_line, ~r/^\s*\}/)
    228 
    229     # -1 to remove the accounted-for `}`
    230     padding = max(padding - 1, 0)
    231 
    232     {line_no, col_start, line_no_end, col_end + padding}
    233   end
    234 
    235   defp find_interpolations(_value, _source) do
    236     nil
    237   end
    238 
    239   if Version.match?(System.version(), ">= 1.12.0-rc") do
    240     # Elixir >= 1.12.0
    241     #
    242     defp determine_heredoc_padding_at_start_of_line(_line), do: 0
    243   else
    244     # Elixir < 1.12.0
    245     #
    246     defp determine_heredoc_padding_at_start_of_line(line),
    247       do: determine_padding_at_start_of_line(line, ~r/^\s+/)
    248   end
    249 
    250   defp determine_padding_at_start_of_line(line, regex) do
    251     regex
    252     |> Regex.run(line)
    253     |> List.wrap()
    254     |> Enum.join()
    255     |> String.length()
    256   end
    257 
    258   defp add_to_col_start_and_end(positions, padding) do
    259     Enum.map(positions, fn {line_no, col_start, line_no_end, col_end} ->
    260       {line_no, col_start + padding, line_no_end, col_end + padding}
    261     end)
    262   end
    263 
    264   defp get_line(source, line_no) do
    265     source
    266     |> String.split("\n")
    267     |> Enum.at(line_no - 1)
    268   end
    269 
    270   defp get_rest_of_line(line, col_end) do
    271     # col-1 to account for col being 1-based
    272     start = max(col_end - 1, 0)
    273 
    274     String.slice(line, start..-1)
    275   end
    276 end