Current section
Files
Jump to
Current section
Files
lib/pattern/no_piped_regex_replace.ex
defmodule Credence.Pattern.NoPipedRegexReplace do
@moduledoc """
Detects `Regex.replace` used as a pipe target and replaces it with
`String.replace`, which accepts the string as its first argument.
`Regex.replace/3` expects `(regex, string, replacement)` — regex first.
When used in a pipeline, the pipe injects the left-hand value as the
first argument, putting the string where the regex should be:
input |> Regex.replace(~r/[^a-z0-9]/, "")
# becomes: Regex.replace(input, ~r/[^a-z0-9]/, "")
# ^^^^^ string in regex position — crash
`String.replace/3` takes `(string, pattern, replacement)` and accepts
regex patterns, so it is a drop-in replacement that works in pipelines:
input |> String.replace(~r/[^a-z0-9]/, "")
"""
@behaviour Credence.Pattern.Rule
@impl true
def priority, do: 50
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{:|>, meta,
[
_left,
{{:., _, [{:__aliases__, _, [:Regex]}, :replace]}, _, _args}
]} = node,
acc ->
{node, [build_issue(meta) | acc]}
node, acc ->
{node, acc}
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, _opts) do
{_ast, patches} =
Macro.prewalk(ast, [], fn
{:|>, _meta,
[
_left,
{{:., _, [{:__aliases__, _, [:Regex]} = alias_node, :replace]}, _, _args}
]} = node,
acc ->
patch = %{
range: Sourceror.get_range(alias_node),
change: "String"
}
{node, [patch | acc]}
node, acc ->
{node, acc}
end)
Enum.reverse(patches)
end
defp build_issue(meta) do
%Credence.Issue{
rule: :no_piped_regex_replace,
message:
"`Regex.replace/3` expects `(regex, string, replacement)` but the pipe " <>
"injects the string as the first argument. Use `String.replace/3` instead, " <>
"which takes the string first and accepts regex patterns.",
meta: %{line: Keyword.get(meta, :line)}
}
end
end