Skip to content

Commit 32f8bc9

Browse files
committed
Do not discard nil on protocol concat, closes #14311
1 parent 65ae623 commit 32f8bc9

File tree

3 files changed

+39
-21
lines changed

3 files changed

+39
-21
lines changed

Diff for: lib/elixir/lib/module.ex

+5-1
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,8 @@ defmodule Module do
950950
@doc """
951951
Concatenates two aliases and returns a new alias.
952952
953-
It handles binaries and atoms.
953+
It handles binaries and atoms. If one of the aliases
954+
is nil, it is discarded.
954955
955956
## Examples
956957
@@ -960,6 +961,9 @@ defmodule Module do
960961
iex> Module.concat(Foo, "Bar")
961962
Foo.Bar
962963
964+
iex> Module.concat(Foo, nil)
965+
Foo
966+
963967
"""
964968
@spec concat(binary | atom, binary | atom) :: atom
965969
def concat(left, right)

Diff for: lib/elixir/lib/protocol.ex

+29-18
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ defmodule Protocol do
394394
end
395395

396396
defp assert_impl!(protocol, base, extra) do
397-
impl = Module.concat(protocol, base)
397+
impl = __concat__(protocol, base)
398398

399399
try do
400400
Code.ensure_compiled!(impl)
@@ -684,14 +684,14 @@ defmodule Protocol do
684684
types
685685
|> List.delete(Any)
686686
|> Enum.map(fn impl ->
687-
{[Module.Types.Of.impl(impl)], Descr.atom([Module.concat(protocol, impl)])}
687+
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
688688
end)
689689

690690
{domain, impl_for, impl_for!} =
691691
case clauses do
692692
[] ->
693693
if Any in types do
694-
clauses = [{[Descr.term()], Descr.atom([Module.concat(protocol, Any)])}]
694+
clauses = [{[Descr.term()], Descr.atom([__concat__(protocol, Any)])}]
695695
{Descr.term(), clauses, clauses}
696696
else
697697
{Descr.none(), [{[Descr.term()], Descr.atom([nil])}],
@@ -707,7 +707,9 @@ defmodule Protocol do
707707
not_domain = Descr.negation(domain)
708708

709709
if Any in types do
710-
clauses = clauses ++ [{[not_domain], Descr.atom([Module.concat(protocol, Any)])}]
710+
clauses =
711+
clauses ++ [{[not_domain], Descr.atom([__concat__(protocol, Any)])}]
712+
711713
{Descr.term(), clauses, clauses}
712714
else
713715
{domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses}
@@ -746,7 +748,7 @@ defmodule Protocol do
746748
end
747749

748750
defp change_impl_for({_name, _kind, meta, _clauses}, protocol, types) do
749-
fallback = if Any in types, do: load_impl(protocol, Any)
751+
fallback = if Any in types, do: __concat__(protocol, Any)
750752
line = meta[:line]
751753

752754
clauses =
@@ -762,7 +764,7 @@ defmodule Protocol do
762764
end
763765

764766
defp change_struct_impl_for({_name, _kind, meta, _clauses}, protocol, types, structs) do
765-
fallback = if Any in types, do: load_impl(protocol, Any)
767+
fallback = if Any in types, do: __concat__(protocol, Any)
766768
clauses = for struct <- structs, do: each_struct_clause_for(struct, protocol, meta)
767769
clauses = clauses ++ [fallback_clause_for(fallback, protocol, meta)]
768770

@@ -772,7 +774,7 @@ defmodule Protocol do
772774
defp built_in_clause_for(mod, guard, protocol, meta, line) do
773775
x = {:x, [line: line, version: -1], __MODULE__}
774776
guard = quote(line: line, do: :erlang.unquote(guard)(unquote(x)))
775-
body = load_impl(protocol, mod)
777+
body = __concat__(protocol, mod)
776778
{meta, [x], [guard], body}
777779
end
778780

@@ -785,17 +787,13 @@ defmodule Protocol do
785787
end
786788

787789
defp each_struct_clause_for(struct, protocol, meta) do
788-
{meta, [struct], [], load_impl(protocol, struct)}
790+
{meta, [struct], [], __concat__(protocol, struct)}
789791
end
790792

791793
defp fallback_clause_for(value, _protocol, meta) do
792794
{meta, [quote(do: _)], [], value}
793795
end
794796

795-
defp load_impl(protocol, for) do
796-
Module.concat(protocol, for)
797-
end
798-
799797
# Finally compile the module and emit its bytecode.
800798
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
801799
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
@@ -957,7 +955,7 @@ defmodule Protocol do
957955
# Define the implementation for built-ins
958956
:lists.foreach(
959957
fn {mod, guard} ->
960-
target = Module.concat(__MODULE__, mod)
958+
target = Protocol.__concat__(__MODULE__, mod)
961959

962960
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
963961
case Code.ensure_compiled(unquote(target)) do
@@ -1001,7 +999,7 @@ defmodule Protocol do
1001999

10021000
# Internal handler for Structs
10031001
Kernel.defp struct_impl_for(struct) do
1004-
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
1002+
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
10051003
{:module, module} -> module
10061004
{:error, _} -> unquote(any_impl_for)
10071005
end
@@ -1074,7 +1072,7 @@ defmodule Protocol do
10741072
quote do
10751073
protocol = unquote(protocol)
10761074
for = unquote(for)
1077-
name = Module.concat(protocol, for)
1075+
name = Protocol.__concat__(protocol, for)
10781076

10791077
Protocol.assert_protocol!(protocol)
10801078
Protocol.__impl__!(protocol, for, __ENV__)
@@ -1120,7 +1118,7 @@ defmodule Protocol do
11201118
else
11211119
# TODO: Deprecate this on Elixir v1.22+
11221120
assert_impl!(protocol, Any, extra)
1123-
{Module.concat(protocol, Any), [for, Macro.struct!(for, env), opts]}
1121+
{__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]}
11241122
end
11251123

11261124
# Clean up variables from eval context
@@ -1132,7 +1130,7 @@ defmodule Protocol do
11321130
else
11331131
__impl__!(protocol, for, env)
11341132
assert_impl!(protocol, Any, extra)
1135-
impl = Module.concat(protocol, Any)
1133+
impl = __concat__(protocol, Any)
11361134

11371135
funs =
11381136
for {fun, arity} <- protocol.__protocol__(:functions) do
@@ -1157,7 +1155,7 @@ defmodule Protocol do
11571155
def __impl__(:for), do: unquote(for)
11581156
end
11591157

1160-
Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env))
1158+
Module.create(__concat__(protocol, for), [quoted | funs], Macro.Env.location(env))
11611159
end
11621160
end)
11631161
end
@@ -1204,4 +1202,17 @@ defmodule Protocol do
12041202
{Reference, :is_reference}
12051203
]
12061204
end
1205+
1206+
@doc false
1207+
def __concat__(left, right) do
1208+
String.to_atom(
1209+
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
1210+
)
1211+
end
1212+
1213+
defp ensure_prefix("Elixir." <> _ = left), do: left
1214+
defp ensure_prefix(left), do: "Elixir." <> left
1215+
1216+
defp remove_prefix("Elixir." <> right), do: right
1217+
defp remove_prefix(right), do: right
12071218
end

Diff for: lib/elixir/test/elixir/protocol_test.exs

+5-2
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,18 @@ defmodule ProtocolTest do
110110
assert Sample.impl_for(%ImplStruct{}) == Sample.ProtocolTest.ImplStruct
111111
assert Sample.impl_for(%ImplStructExplicitFor{}) == Sample.ProtocolTest.ImplStructExplicitFor
112112
assert Sample.impl_for(%NoImplStruct{}) == nil
113+
assert is_nil(Sample.impl_for(%{__struct__: nil}))
113114
end
114115

115116
test "protocol implementation with Any and struct fallbacks" do
116117
assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any
117-
# Derived
118-
assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct
118+
assert WithAny.impl_for(%{__struct__: nil}) == WithAny.Any
119119
assert WithAny.impl_for(%{__struct__: "foo"}) == WithAny.Map
120120
assert WithAny.impl_for(%{}) == WithAny.Map
121121
assert WithAny.impl_for(self()) == WithAny.Any
122+
123+
# Derived
124+
assert WithAny.impl_for(%ImplStruct{}) == ProtocolTest.WithAny.ProtocolTest.ImplStruct
122125
end
123126

124127
test "protocol not implemented" do

0 commit comments

Comments
 (0)