Skip to content

Commit fa2a19e

Browse files
committed
Introduce protocols: argument to reenable/1
1 parent 235fec9 commit fa2a19e

File tree

2 files changed

+191
-1
lines changed

2 files changed

+191
-1
lines changed

lib/mix/lib/mix/tasks/compile.ex

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,30 @@ defmodule Mix.Tasks.Compile do
9090
9191
By default reenables all compilers.
9292
"""
93-
@spec reenable(compilers: compilers) :: :ok when compilers: :all | [atom()]
93+
@spec reenable([{:compilers, compilers} | {:protocols, protocols}]) :: :ok
94+
when compilers: :all | [atom()], protocols: :consolidate | nil
9495
def reenable(opts \\ []) do
9596
compilers =
9697
case Keyword.get(opts, :compilers, :all) do
9798
:all -> compilers()
9899
list when is_list(list) -> list
99100
end
100101

102+
compilers =
103+
if Keyword.get(opts, :protocols) in [:consolidate, true] do
104+
compilers
105+
|> Enum.map(&to_string/1)
106+
|> Enum.reduce([], fn
107+
"elixir", acc -> ["protocols", "elixir" | acc]
108+
"app", acc -> ["app", "protocols", "elixir" | acc]
109+
other, acc -> [other | acc]
110+
end)
111+
|> Enum.reverse()
112+
|> Enum.uniq()
113+
else
114+
compilers
115+
end
116+
101117
Enum.each(["compile", "compile.all"], &Mix.Task.reenable(&1))
102118
Enum.each(compilers, &Mix.Task.reenable("compile.#{&1}"))
103119
end
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
Code.require_file("../../test_helper.exs", __DIR__)
2+
3+
defmodule Mix.Tasks.Compile.ProtocolsTest do
4+
use MixTest.Case
5+
6+
@old {{2010, 1, 1}, {0, 0, 0}}
7+
8+
test "compiles and consolidates local protocols", context do
9+
in_tmp(context.test, fn ->
10+
Mix.Project.push(MixTest.Case.Sample)
11+
12+
File.mkdir_p!("lib")
13+
assert Mix.Task.run("compile")
14+
15+
# Define a local protocol
16+
File.write!("lib/protocol.ex", """
17+
defprotocol Compile.Protocol do
18+
def foo(a, b)
19+
end
20+
""")
21+
22+
assert compile_elixir_and_protocols() == :ok
23+
assert apply(Compile.Protocol, :__protocol__, [:impls]) == {:consolidated, []}
24+
mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
25+
26+
# Implement a local protocol
27+
File.write!("lib/impl.ex", """
28+
defimpl Compile.Protocol, for: Integer do
29+
def foo(a, b), do: a + b
30+
end
31+
""")
32+
33+
assert compile_elixir_and_protocols() == :ok
34+
assert apply(Compile.Protocol, :__protocol__, [:impls]) == {:consolidated, [Integer]}
35+
36+
assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") !=
37+
@old
38+
39+
# Delete a local implementation
40+
File.rm!("lib/impl.ex")
41+
assert compile_elixir_and_protocols() == :ok
42+
assert apply(Compile.Protocol, :__protocol__, [:impls]) == {:consolidated, []}
43+
44+
assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") !=
45+
@old
46+
47+
# Delete a local protocol
48+
File.rm!("lib/protocol.ex")
49+
assert compile_elixir_and_protocols() == :ok
50+
refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
51+
end)
52+
end
53+
54+
test "compiles after converting a protocol into a standard module", context do
55+
in_tmp(context.test, fn ->
56+
Mix.Project.push(MixTest.Case.Sample)
57+
58+
File.mkdir_p!("lib")
59+
Mix.Task.run("compile")
60+
purge_protocol(Compile.Protocol)
61+
62+
# Define a local protocol
63+
File.write!("lib/protocol.ex", """
64+
defprotocol Compile.Protocol do
65+
def foo(a)
66+
end
67+
68+
defimpl Compile.Protocol, for: Integer do
69+
def foo(a), do: a
70+
end
71+
""")
72+
73+
assert compile_elixir_and_protocols() == :ok
74+
assert apply(Compile.Protocol, :__protocol__, [:impls]) == {:consolidated, [Integer]}
75+
mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
76+
File.rm!("lib/protocol.ex")
77+
78+
# Define a standard module
79+
File.write!("lib/protocol.ex", """
80+
defmodule Compile.Protocol do
81+
end
82+
""")
83+
84+
assert compile_elixir_and_protocols() == :ok
85+
refute function_exported?(Compile.Protocol, :__protocol__, 1)
86+
87+
# Delete a local protocol
88+
File.rm!("lib/protocol.ex")
89+
assert compile_elixir_and_protocols() == :ok
90+
refute function_exported?(Compile.Protocol, :__protocol__, 1)
91+
refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
92+
end)
93+
end
94+
95+
test "compiles and consolidates deps protocols", context do
96+
in_tmp(context.test, fn ->
97+
Mix.Project.push(MixTest.Case.Sample)
98+
99+
File.mkdir_p!("lib")
100+
Mix.Task.run("compile")
101+
purge_protocol(String.Chars)
102+
mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam")
103+
104+
assert compile_elixir_and_protocols() == :ok
105+
assert {:consolidated, [Atom | _]} = apply(String.Chars, :__protocol__, [:impls])
106+
assert mtime("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") == @old
107+
108+
# Implement a deps protocol
109+
File.write!("lib/struct.ex", """
110+
defmodule Aaaaa do
111+
defstruct a: nil
112+
defimpl String.Chars do
113+
def to_string(_), do: "ok"
114+
end
115+
end
116+
""")
117+
118+
assert compile_elixir_and_protocols() == :ok
119+
assert {:consolidated, [Aaaaa, Atom | _]} = apply(String.Chars, :__protocol__, [:impls])
120+
assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != @old
121+
122+
# Delete the local implementation
123+
File.rm!("lib/struct.ex")
124+
assert compile_elixir_and_protocols() == :ok
125+
assert {:consolidated, [Atom | _]} = apply(String.Chars, :__protocol__, [:impls])
126+
assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != @old
127+
end)
128+
end
129+
130+
test "consolidated protocols keep relative path to their source" do
131+
in_fixture("no_mixfile", fn ->
132+
Mix.Project.push(MixTest.Case.Sample)
133+
compile_elixir_and_protocols()
134+
135+
# Load consolidated
136+
:code.add_patha(~c"_build/dev/lib/sample/consolidated")
137+
:code.purge(Enumerable)
138+
:code.delete(Enumerable)
139+
140+
try do
141+
Enumerable.impl_for!(:oops)
142+
rescue
143+
Protocol.UndefinedError ->
144+
assert [{_, _, _, [file: ~c"lib/enum.ex"] ++ _} | _] = __STACKTRACE__
145+
else
146+
_ ->
147+
flunk("Enumerable.impl_for!/1 should have failed")
148+
after
149+
purge_protocol(Enumerable)
150+
end
151+
end)
152+
end
153+
154+
defp compile_elixir_and_protocols do
155+
Mix.Tasks.Compile.reenable(protocols: :consolidate)
156+
with {result, _} <- Mix.Task.run("compile"), do: result
157+
end
158+
159+
defp mtime(path) do
160+
File.stat!(path).mtime
161+
end
162+
163+
defp mark_as_old!(path) do
164+
mtime = mtime(path)
165+
File.touch!(path, @old)
166+
mtime
167+
end
168+
169+
defp purge_protocol(module) do
170+
:code.del_path(:filename.absname(~c"_build/dev/lib/sample/consolidated"))
171+
:code.purge(module)
172+
:code.delete(module)
173+
end
174+
end

0 commit comments

Comments
 (0)