Current section
Files
Jump to
Current section
Files
lib/credo/code/interpolation_helper.ex
defmodule Credo.Code.InterpolationHelper do
@moduledoc false
alias Credo.Code.Token
@doc false
def replace_interpolations(source, char \\ " ") do
positions = interpolation_positions(source)
lines = String.split(source, "\n")
positions
|> Enum.reverse()
|> Enum.reduce(lines, &replace_line(&1, &2, char))
|> Enum.join("\n")
end
defp replace_line({line_no, col_start, line_no, col_end}, lines, char) do
List.update_at(
lines,
line_no - 1,
&replace_line(&1, col_start, col_end, char)
)
end
defp replace_line(position, lines, char) do
{line_no_start, col_start, line_no_end, col_end} = position
Enum.reduce(line_no_start..line_no_end, lines, fn
line_no, memo
when line_no == line_no_start ->
List.update_at(
memo,
line_no - 1,
&replace_line(&1, col_start, String.length(&1) + 1, char)
)
line_no, memo
when line_no == line_no_end ->
List.update_at(
memo,
line_no - 1,
&replace_line(&1, 1, col_end, char)
)
line_no, memo
when line_no < line_no_end ->
List.update_at(
memo,
line_no - 1,
&replace_line(&1, 1, String.length(&1) + 1, char)
)
end)
end
defp replace_line(line, col_start, col_end, char) do
length = max(col_end - col_start, 0)
String.slice(line, 0, col_start - 1) <>
String.duplicate(char, length) <> String.slice(line, (col_end - 1)..-1)
end
@doc false
def interpolation_positions(source) do
source
|> Credo.Code.to_tokens()
|> Enum.flat_map(&map_interpolations(&1, source))
|> Enum.reject(&is_nil/1)
end
if Version.match?(System.version(), ">= 1.6.0-rc") do
#
# Elixir >= 1.6.0
#
defp map_interpolations(
{:sigil, {_line_no, _col_start, nil}, _, list, _, _sigil_start_char} = token,
source
) do
handle_atom_string_or_sigil(token, list, source)
end
defp map_interpolations(
{:bin_heredoc, {_line_no, _col_start, _}, _list} = token,
source
) do
handle_heredoc(token, source)
end
defp map_interpolations(
{:bin_string, {_line_no, _col_start, _}, list} = token,
source
) do
handle_atom_string_or_sigil(token, list, source)
end
else
#
# Elixir <= 1.5.x
#
defp is_sigil_in_line(source, line_no) do
line_with_heredoc_quotes = get_line(source, line_no)
!!Regex.run(~r/("""|''')/, line_with_heredoc_quotes)
end
defp map_interpolations(
{:sigil, {_line_no, _col_start, _col_end}, _, list, _} = token,
source
) do
handle_atom_string_or_sigil(token, list, source)
end
defp map_interpolations(
{:bin_string, {line_no, _col_start, _}, list} = token,
source
) do
if is_sigil_in_line(source, line_no) do
handle_heredoc(token, source)
else
handle_atom_string_or_sigil(token, list, source)
end
end
end
defp map_interpolations(
{:atom_unsafe, {_line_no, _col_start, _}, list} = token,
source
) do
handle_atom_string_or_sigil(token, list, source)
end
defp map_interpolations(_, _source), do: []
defp handle_atom_string_or_sigil(_token, list, source) do
find_interpolations(list, source)
end
defp handle_heredoc({_atom, {line_no, _, _}, list}, source) do
first_line_in_heredoc = get_line(source, line_no + 1)
# TODO: this seems to be wrong. the closing """ determines the
# indentation, not the first line of the heredoc.
padding_in_first_line = determine_padding_at_start_of_line(first_line_in_heredoc)
list
|> find_interpolations(source)
|> Enum.reject(&is_nil/1)
|> add_to_col_start_and_end(padding_in_first_line)
end
defp find_interpolations(list, source) when is_list(list) do
Enum.map(list, &find_interpolations(&1, source))
end
# {{1, 25, 32}, [{:identifier, {1, 27, 31}, :name}]}
defp find_interpolations({{_line_no, _col_start2, _}, _list} = token, source) do
{line_no, col_start, line_no_end, col_end} = Token.position(token)
{line_no, col_start, line_no_end, col_end}
# |> IO.inspect()
col_end =
if line_no_end > line_no && col_end == 1 do
# This means we encountered :eol and jumped in the next line.
# We need to add the closing `}`.
col_end + 1
else
col_end
end
line = get_line(source, line_no_end)
# `col_end - 1` to account for the closing `}`
rest_of_line = get_rest_of_line(line, col_end - 1)
# IO.inspect(rest_of_line, label: "rest_of_line")
padding = determine_padding_at_start_of_line(rest_of_line, ~r/^\s*\}/)
# -1 to remove the accounted-for `}`
padding = max(padding - 1, 0)
# IO.inspect(padding, label: "padding")
{line_no, col_start, line_no_end, col_end + padding}
end
defp find_interpolations(_value, _source), do: nil
defp determine_padding_at_start_of_line(line, regex \\ ~r/^\s+/) do
regex
|> Regex.run(line)
|> List.wrap()
|> Enum.join()
|> String.length()
end
defp add_to_col_start_and_end(positions, padding) do
Enum.map(positions, fn {line_no, col_start, line_no_end, col_end} ->
{line_no, col_start + padding, line_no_end, col_end + padding}
end)
end
defp get_line(source, line_no) do
source
|> String.split("\n")
|> Enum.at(line_no - 1)
end
defp get_rest_of_line(line, col_end) do
# col-1 to account for col being 1-based
start = max(col_end - 1, 0)
String.slice(line, start..-1)
end
end