Skip to content

Commit

Permalink
Do not use deprecated Macro.to_string/2
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed May 27, 2021
1 parent 42e3e69 commit d9b2f1a
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 81 deletions.
158 changes: 82 additions & 76 deletions lib/ecto/query/inspect.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ defimpl Inspect, for: Ecto.Query.DynamicExpr do
{expr, binding, params, subqueries, _, _} =
Ecto.Query.Builder.Dynamic.fully_expand(query, dynamic)

names = Enum.map(binding, fn
{_, {name, _, _}} -> Atom.to_string(name)
{name, _, _} -> Atom.to_string(name)
end)
names =
Enum.map(binding, fn
{_, {name, _, _}} -> name
{name, _, _} -> name
end)

query_expr = %{expr: expr, params: params, subqueries: subqueries}
inspected = Inspect.Ecto.Query.expr(expr, List.to_tuple(names), query_expr)
Expand Down Expand Up @@ -83,12 +84,12 @@ defimpl Inspect, for: Ecto.Query do
defp to_list(query) do
names =
query
|> collect_sources
|> generate_letters
|> generate_names
|> collect_sources()
|> generate_letters()
|> generate_names()
|> List.to_tuple()

from = bound_from(query.from, binding(names, 0))
from = bound_from(query.from, elem(names, 0))
joins = joins(query.joins, names)
preloads = preloads(query.preloads)
assocs = assocs(query.assocs, names)
Expand Down Expand Up @@ -145,11 +146,11 @@ defimpl Inspect, for: Ecto.Query do
defp joins(joins, names) do
joins
|> Enum.with_index()
|> Enum.flat_map(fn {expr, ix} -> join(expr, binding(names, expr.ix || ix + 1), names) end)
|> Enum.flat_map(fn {expr, ix} -> join(expr, elem(names, expr.ix || ix + 1), names) end)
end

defp join(%JoinExpr{qual: qual, assoc: {ix, right}, on: on} = join, name, names) do
string = "#{name} in assoc(#{binding(names, ix)}, #{inspect(right)})"
string = "#{name} in assoc(#{elem(names, ix)}, #{inspect(right)})"
[{join_qual(qual), string}] ++ kw_as_and_prefix(join) ++ maybe_on(on, names)
end

Expand Down Expand Up @@ -224,106 +225,111 @@ defimpl Inspect, for: Ecto.Query do

@doc false
def expr(expr, names, part) do
Macro.to_string(expr, &expr_to_string(&1, &2, names, part))
# TODO: Actually use quoted_to_algebra on Elixir v1.12+
expr
|> Macro.traverse(:ok, &{prewalk(&1), &2}, &{postwalk(&1, names, part), &2})
|> elem(0)
|> Macro.to_string()
end

# For keyword and interpolated fragments use normal escaping
defp expr_to_string({:fragment, _, [{_, _} | _] = parts}, _, names, part) do
"fragment(" <> unmerge_fragments(parts, "", [], names, part) <> ")"
# Tagged values
defp prewalk(%Ecto.Query.Tagged{value: value, tag: nil}) do
value
end

# Convert variables to proper names
defp expr_to_string({:&, _, [ix]}, _, names, %{take: take}) do
case take do
%{^ix => {:any, fields}} when ix == 0 ->
Kernel.inspect(fields)

%{^ix => {tag, fields}} ->
"#{tag}(" <> binding(names, ix) <> ", " <> Kernel.inspect(fields) <> ")"
defp prewalk(%Ecto.Query.Tagged{value: value, tag: tag}) do
{:type, [], [value, tag]}
end

_ ->
binding(names, ix)
end
defp prewalk(node) do
node
end

defp expr_to_string({:&, _, [ix]}, _, names, _) do
binding(names, ix)
# Convert variables to proper names
defp postwalk({:&, _, [ix]}, names, part) do
binding_to_expr(ix, names, part)
end

# Inject the interpolated value
#
# In case the query had its parameters removed,
# we use ... to express the interpolated code.
defp expr_to_string({:^, _, [_ix, _len]}, _, _, _part) do
Macro.to_string({:^, [], [{:..., [], nil}]})
# Remove parens from field calls
defp postwalk({{:., _, [_, _]} = dot, meta, []}, _names, _part) do
{dot, [no_parens: true] ++ meta, []}
end

defp expr_to_string({:^, _, [ix]}, _, _, %{params: params}) do
case Enum.at(params || [], ix) do
{value, _type} -> "^" <> Kernel.inspect(value, charlists: :as_lists)
_ -> "^..."
end
# Interpolated unknown value
defp postwalk({:^, _, [_ix, _len]}, _names, _part) do
{:^, [], [{:..., [], nil}]}
end

# Strip trailing ()
defp expr_to_string({{:., _, [_, _]}, _, []}, string, _, _) do
size = byte_size(string)
:binary.part(string, 0, size - 2)
# Interpolated known value
defp postwalk({:^, _, [ix]}, _, %{params: params}) do
value =
case Enum.at(params || [], ix) do
# Wrap the head in a block so it is not treated as a charlist
{[head | tail], _type} -> [{:__block__, [], [head]} | tail]
{value, _type} -> value
_ -> {:..., [], nil}
end

{:^, [], [value]}
end

# Types need to be converted back to AST for fields
defp expr_to_string({:type, [], [expr, type]}, _string, names, part) do
"type(#{expr(expr, names, part)}, #{type |> type_to_expr() |> expr(names, part)})"
defp postwalk({:type, meta, [expr, type]}, names, part) do
{:type, meta, [expr, type_to_expr(type, names, part)]}
end

# Tagged values
defp expr_to_string(%Ecto.Query.Tagged{value: value, tag: nil}, _, _names, _) do
inspect(value)
# For keyword and interpolated fragments use normal escaping
defp postwalk({:fragment, _, [{_, _} | _] = parts}, _names, _part) do
{:fragment, [], unmerge_fragments(parts, "", [])}
end

defp expr_to_string(%Ecto.Query.Tagged{value: value, tag: tag}, _, names, part) do
{:type, [], [value, tag]} |> expr(names, part)
# Subqueries
defp postwalk({:subquery, i}, _names, %{subqueries: subqueries}) do
{:subquery, [], [Enum.fetch!(subqueries, i).query]}
end

defp expr_to_string({:json_extract_path, _, [expr, path]}, _, names, part) do
json_expr_path_to_expr(expr, path) |> expr(names, part)
# Jason
defp postwalk({:json_extract_path, _, [expr, path]}, _names, _part) do
Enum.reduce(path, expr, fn element, acc ->
{{:., [], [Access, :get]}, [], [acc, element]}
end)
end

defp expr_to_string({:{}, [], [:subquery, i]}, _string, _names, %{subqueries: subqueries}) do
# We were supposed to match on {:subquery, i} but Elixir incorrectly
# translates those to `:{}` when converting to string.
# See https://github.com/elixir-lang/elixir/blob/27bd9ffcc607b74ce56b547cb6ba92c9012c317c/lib/elixir/lib/macro.ex#L932
inspect_source(Enum.fetch!(subqueries, i))
defp postwalk(node, _names, _part) do
node
end

defp expr_to_string(_expr, string, _, _) do
string
end
defp binding_to_expr(ix, names, part) do
case part do
%{take: %{^ix => {:any, fields}}} when ix == 0 ->
fields

%{take: %{^ix => {tag, fields}}} ->
{tag, [], [binding(names, ix), fields]}

defp type_to_expr({composite, type}) when is_atom(composite) do
{composite, type_to_expr(type)}
_ ->
binding(names, ix)
end
end

defp type_to_expr({part, type}) when is_integer(part) do
{{:., [], [{:&, [], [part]}, type]}, [], []}
defp type_to_expr({ix, type}, names, part) when is_integer(ix) do
{{:., [], [binding_to_expr(ix, names, part), type]}, [no_parens: true], []}
end

defp type_to_expr(type) do
type
defp type_to_expr({composite, type}, names, part) when is_atom(composite) do
{composite, type_to_expr(type, names, part)}
end

defp json_expr_path_to_expr(expr, path) do
Enum.reduce(path, expr, fn element, acc ->
{{:., [], [Access, :get]}, [], [acc, element]}
end)
defp type_to_expr(type, _names, _part) do
type
end

defp unmerge_fragments([{:raw, s}, {:expr, v} | t], frag, args, names, part) do
unmerge_fragments(t, frag <> s <> "?", [expr(v, names, part) | args], names, part)
defp unmerge_fragments([{:raw, s}, {:expr, v} | t], frag, args) do
unmerge_fragments(t, frag <> s <> "?", [v | args])
end

defp unmerge_fragments([{:raw, s}], frag, args, _names, _part) do
Enum.join([inspect(frag <> s) | Enum.reverse(args)], ", ")
defp unmerge_fragments([{:raw, s}], frag, args) do
[frag <> s | Enum.reverse(args)]
end

defp join_qual(:inner), do: :join
Expand Down Expand Up @@ -375,15 +381,15 @@ defimpl Inspect, for: Ecto.Query do
end

defp generate_names(letters) do
{names, _} = Enum.map_reduce(letters, 0, &{"#{&1}#{&2}", &2 + 1})
{names, _} = Enum.map_reduce(letters, 0, &{:"#{&1}#{&2}", &2 + 1})
names
end

defp binding(names, pos) do
try do
elem(names, pos)
{elem(names, pos), [], nil}
rescue
ArgumentError -> "unknown_binding_#{pos}!"
ArgumentError -> {:"unknown_binding_#{pos}!", [], nil}
end
end

Expand Down
10 changes: 5 additions & 5 deletions test/ecto/query/inspect_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ defmodule Ecto.Query.InspectTest do
sq = from(Post, [])
dynamic = dynamic([o, b], b.user_id == ^1 or b.user_id in subquery(sq))
assert inspect(dynamic([o], o.type == ^2 and ^dynamic)) ==
"dynamic([o, b], o.type == ^2 and (b.user_id == ^1 or b.user_id in subquery(from p0 in Inspect.Post)))"
"dynamic([o, b], o.type == ^2 and (b.user_id == ^1 or b.user_id in subquery(#Ecto.Query<from p0 in Inspect.Post>)))"
end

test "invalid query" do
Expand Down Expand Up @@ -184,7 +184,7 @@ defmodule Ecto.Query.InspectTest do
test "where in subquery" do
s = from(x in Post, where: x.bar == ^"1", select: x.foo)
assert i(from(x in Post, where: x.foo in subquery(s))) ==
~s{from p0 in Inspect.Post, where: p0.foo in subquery(from p0 in Inspect.Post,\n where: p0.bar == ^"1",\n select: p0.foo)}
~s{from p0 in Inspect.Post, where: p0.foo in subquery(#Ecto.Query<from p0 in Inspect.Post, where: p0.bar == ^"1", select: p0.foo>)}
end

test "group by" do
Expand Down Expand Up @@ -350,11 +350,11 @@ defmodule Ecto.Query.InspectTest do

test "container values" do
assert i(from(Post, select: <<1, 2, 3>>)) ==
"from p0 in Inspect.Post, select: <<1, 2, 3>>"
"from p0 in Inspect.Post, select: \"\\x01\\x02\\x03\""

foo = <<1, 2, 3>>
assert i(from(p in Post, select: {p, ^foo})) ==
"from p0 in Inspect.Post, select: {p0, ^<<1, 2, 3>>}"
"from p0 in Inspect.Post, select: {p0, ^\"\\x01\\x02\\x03\"}"
end

test "select" do
Expand Down Expand Up @@ -413,7 +413,7 @@ defmodule Ecto.Query.InspectTest do
end

def i(query) do
assert "#Ecto.Query<" <> rest = inspect query
assert "#Ecto.Query<" <> rest = inspect(query, safe: false)
size = byte_size(rest)
assert ">" = :binary.part(rest, size - 1, 1)
:binary.part(rest, 0, size - 1)
Expand Down

0 comments on commit d9b2f1a

Please sign in to comment.