Packages
credo
1.7.19
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.7-rc.0
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.2-rc.4
1.7.2-rc.3
1.7.2-rc.2
1.7.2-rc.1
1.7.2-rc.0
1.7.1
1.7.0
1.7.0-rc.2
1.7.0-rc.1
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.6.0-rc.1
1.6.0-rc.0
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.5.0-rc.5
1.5.0-rc.4
1.5.0-rc.3
1.5.0-rc.2
1.5.0-rc.1
1.4.1
1.4.0
1.4.0-rc.2
1.4.0-rc.1
1.3.2
1.3.1
1.3.0
1.3.0-rc3
1.3.0-rc2
1.3.0-rc1
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc4
1.2.0-rc3
1.2.0-rc2
1.2.0-rc1
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc3
1.1.0-rc2
1.1.0-rc1
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.1-rc1
1.0.0
1.0.0-rc1
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.9.0-rc8
0.9.0-rc7
0.9.0-rc6
0.9.0-rc5
0.9.0-rc4
0.9.0-rc3
0.9.0-rc2
0.9.0-rc1
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.8.0-rc7
0.8.0-rc6
0.8.0-rc5
0.8.0-rc4
0.8.0-rc3
0.8.0-rc2
0.8.0-rc1
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.6.0-rc2
0.6.0-rc1
0.5.3
0.5.2
0.5.1
0.5.0
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.10-dev
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.4.0-beta5
0.4.0-beta4
0.4.0-beta3
0.4.0-beta2
0.4.0-beta1
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-dev2
0.3.0-dev
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.1-dev
A static code analysis tool with a focus on code consistency and teaching.
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 \\ " ", filename \\ "nofilename")
when is_binary(source) do
positions = interpolation_positions(source, filename)
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)
line = String.to_charlist(line)
part1 = Enum.slice(line, 0, col_start - 1)
part2 = String.to_charlist(String.duplicate(char, length))
part3 = Enum.slice(line, (col_end - 1)..-1//1)
List.to_string(part1 ++ part2 ++ part3)
end
@doc false
def interpolation_positions(source, filename \\ "nofilename") do
source
|> Credo.Code.to_tokens(filename)
|> Enum.flat_map(&map_interpolations(&1, source))
|> Enum.reject(&is_nil/1)
end
#
# Elixir >= 1.13.0
#
defp map_interpolations(
{:bin_heredoc, {_line_no, _col_start, _}, _, _list} = token,
source
) do
handle_heredoc(token, source)
end
#
# Elixir >= 1.11.0
#
defp map_interpolations(
{:sigil, {_line_no, _col_start, _end_position}, _, list, _empty_list, nil,
_another_binary} = token,
source
) do
handle_atom_string_or_sigil(token, list, source)
end
#
# 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
defp map_interpolations(
{:kw_identifier_unsafe, {_line_no, _col_start, _}, list} = token,
source
) do
handle_atom_string_or_sigil(token, list, source)
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(_token, _source) do
[]
end
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_heredoc_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
# Elixir 1.13+
defp handle_heredoc({atom, {line_no, col_start, col_end}, _, list}, source) do
handle_heredoc({atom, {line_no, col_start, col_end}, list}, source)
end
defp find_interpolations(list, source) when is_list(list) do
Enum.map(list, &find_interpolations(&1, source))
end
# Elixir < 1.9.0
#
defp find_interpolations(
{{line_no, col_start, nil}, {line_no_end, col_end, nil}, list} = _token,
_source
)
when is_list(list) do
{line_no, col_start, line_no_end, col_end + 1}
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)
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
# Elixir >= 1.9.0
#
# {{1, 25, nil}, {1, 31, nil}, [{:identifier, {1, 27, nil}, :name}]}
defp find_interpolations(
{{_line_no, _col_start, nil}, {_line_no2, _col_start2, nil}, _list} = token,
source
) do
{line_no, col_start, line_no_end, col_end} = Token.position(token)
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)
{line_no, col_start, line_no_end, col_end + padding}
end
defp find_interpolations(_value, _source) do
nil
end
if Version.match?(System.version(), ">= 1.12.0-rc") do
# Elixir >= 1.12.0
#
defp determine_heredoc_padding_at_start_of_line(_line), do: 0
else
# Elixir < 1.12.0
#
defp determine_heredoc_padding_at_start_of_line(line),
do: determine_padding_at_start_of_line(line, ~r/^\s+/)
end
defp determine_padding_at_start_of_line(line, regex) 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