Skip to content

Commit ea185b8

Browse files
authored
Add Module.get_last_attribute/3 (#12611)
1 parent 9ea33bf commit ea185b8

File tree

3 files changed

+115
-8
lines changed

3 files changed

+115
-8
lines changed

lib/elixir/lib/module.ex

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,10 +1508,42 @@ defmodule Module do
15081508
"""
15091509
@spec get_attribute(module, atom, term) :: term
15101510
def get_attribute(module, key, default \\ nil) when is_atom(module) and is_atom(key) do
1511-
case __get_attribute__(module, key, nil, true) do
1512-
nil -> default
1513-
value -> value
1514-
end
1511+
get_attribute(module, key, nil, true, false, default, {:get_attribute, 2})
1512+
end
1513+
1514+
@doc """
1515+
Gets the last set value of a given attribute from a module.
1516+
1517+
If the attribute was marked with `accumulate` with
1518+
`Module.register_attribute/3`, the previous value to have been set will be
1519+
returned. If the attribute does not accumulate, this call is the same as
1520+
calling `Module.get_attribute/3`.
1521+
1522+
This function can only be used on modules that have not yet been compiled.
1523+
Use the `c:Module.__info__/1` callback to get all persisted attributes, or
1524+
`Code.fetch_docs/1` to retrieve all documentation related attributes in
1525+
compiled modules.
1526+
1527+
## Examples
1528+
1529+
defmodule Foo do
1530+
Module.put_attribute(__MODULE__, :value, 1)
1531+
Module.get_last_attribute(__MODULE__, :value) #=> 1
1532+
1533+
Module.get_last_attribute(__MODULE__, :not_found, :default) #=> :default
1534+
1535+
Module.register_attribute(__MODULE__, :acc, accumulate: true)
1536+
Module.put_attribute(__MODULE__, :acc, 1)
1537+
Module.get_last_attribute(__MODULE__, :acc) #=> 1
1538+
Module.put_attribute(__MODULE__, :acc, 2)
1539+
Module.get_last_attribute(__MODULE__, :acc) #=> 2
1540+
end
1541+
1542+
"""
1543+
@doc since: "1.15.0"
1544+
@spec get_last_attribute(module, atom, term) :: term
1545+
def get_last_attribute(module, key, default \\ nil) when is_atom(module) and is_atom(key) do
1546+
get_attribute(module, key, nil, true, true, default, {:get_last_attribute, 2})
15151547
end
15161548

15171549
@doc """
@@ -1871,8 +1903,20 @@ defmodule Module do
18711903
# Used internally by Kernel's @.
18721904
# This function is private and must be used only internally.
18731905
def __get_attribute__(module, key, caller_line, trace?) when is_atom(key) do
1906+
get_attribute(module, key, caller_line, trace?, false, nil, {:get_attribute, 2})
1907+
end
1908+
1909+
defp get_attribute(
1910+
module,
1911+
key,
1912+
caller_line,
1913+
trace?,
1914+
last_accumulated?,
1915+
default,
1916+
function_name_arity
1917+
) do
18741918
assert_not_compiled!(
1875-
{:get_attribute, 2},
1919+
function_name_arity,
18761920
module,
18771921
"Use the Module.__info__/1 callback or Code.fetch_docs/1 instead"
18781922
)
@@ -1882,7 +1926,7 @@ defmodule Module do
18821926
case :ets.lookup(set, key) do
18831927
[{_, _, :accumulate, traces}] ->
18841928
trace_attribute(trace?, module, traces, set, key, [])
1885-
:lists.reverse(bag_lookup_element(bag, {:accumulate, key}, 2))
1929+
lookup_accumulate_attribute(bag, key, default, last_accumulated?)
18861930

18871931
[{_, value, warn_line, traces}] when is_integer(warn_line) ->
18881932
trace_attribute(trace?, module, traces, set, key, [{3, :used}])
@@ -1899,10 +1943,21 @@ defmodule Module do
18991943
"please remove access to @#{key} or explicitly set it before access"
19001944

19011945
IO.warn(error_message, attribute_stack(module, caller_line))
1902-
nil
1946+
default
19031947

19041948
[] ->
1905-
nil
1949+
default
1950+
end
1951+
end
1952+
1953+
defp lookup_accumulate_attribute(bag, key, _default, false) do
1954+
:lists.reverse(bag_lookup_element(bag, {:accumulate, key}, 2))
1955+
end
1956+
1957+
defp lookup_accumulate_attribute(bag, key, default, true) do
1958+
case :ets.select(bag, [{{{:accumulate, key}, :"$1"}, [], [:"$1"]}], 1) do
1959+
{[value], _} -> value
1960+
_ -> default
19061961
end
19071962
end
19081963

lib/elixir/test/elixir/kernel/lexical_tracker_test.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,21 @@ defmodule Kernel.LexicalTrackerTest do
238238

239239
refute String in compile
240240
refute Enum in compile
241+
242+
{{compile, _, _, _}, _binding} =
243+
Code.eval_string("""
244+
defmodule Kernel.LexicalTrackerTest.Attribute7 do
245+
Module.register_attribute(__MODULE__, :example, accumulate: true)
246+
@example String
247+
@example Enum
248+
_ = Module.get_last_attribute(__MODULE__, :example)
249+
250+
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
251+
end |> elem(3)
252+
""")
253+
254+
assert String in compile
255+
assert Enum in compile
241256
end
242257

243258
test "@compile adds a runtime dependency" do

lib/elixir/test/elixir/module_test.exs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,8 @@ defmodule ModuleTest do
566566
Module.put_attribute(__MODULE__, :attribute, 1)
567567
assert Module.get_attribute(__MODULE__, :attribute) == 1
568568
assert Module.get_attribute(__MODULE__, :attribute, :default) == 1
569+
Module.put_attribute(__MODULE__, :attribute, nil)
570+
assert Module.get_attribute(__MODULE__, :attribute, :default) == nil
569571
end
570572
end
571573

@@ -576,6 +578,41 @@ defmodule ModuleTest do
576578
end
577579
end
578580

581+
describe "get_last_attribute/3" do
582+
test "returns the last set value when the attribute is marked as `accumulate: true`" do
583+
in_module do
584+
Module.register_attribute(__MODULE__, :value, accumulate: true)
585+
Module.put_attribute(__MODULE__, :value, 1)
586+
assert Module.get_last_attribute(__MODULE__, :value) == 1
587+
Module.put_attribute(__MODULE__, :value, 2)
588+
assert Module.get_last_attribute(__MODULE__, :value) == 2
589+
end
590+
end
591+
592+
test "returns the value of the non-accumulate attribute if it exists" do
593+
in_module do
594+
Module.put_attribute(__MODULE__, :attribute, 1)
595+
assert Module.get_last_attribute(__MODULE__, :attribute) == 1
596+
Module.put_attribute(__MODULE__, :attribute, nil)
597+
assert Module.get_last_attribute(__MODULE__, :attribute, :default) == nil
598+
end
599+
end
600+
601+
test "returns the passed default if the accumulate attribute has not yet been set" do
602+
in_module do
603+
Module.register_attribute(__MODULE__, :value, accumulate: true)
604+
assert Module.get_last_attribute(__MODULE__, :value) == nil
605+
assert Module.get_last_attribute(__MODULE__, :value, :default) == :default
606+
end
607+
end
608+
609+
test "returns the passed default if the non-accumulate attribute does not exist" do
610+
in_module do
611+
assert Module.get_last_attribute(__MODULE__, :value, :default) == :default
612+
end
613+
end
614+
end
615+
579616
describe "has_attribute?/2 and attributes_in/2" do
580617
test "returns true when attribute has been defined" do
581618
in_module do

0 commit comments

Comments
 (0)