diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index f251e7b722..06895ce235 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -549,6 +549,11 @@ defmodule Module do :elixir_module.is_open(module) end + @spec not_closing?(module) :: boolean + defp not_closing?(module) when is_atom(module) do + :elixir_module.is_not_closing(module) + end + @doc """ Evaluates the quoted contents in the given module's context. @@ -607,7 +612,7 @@ defmodule Module do def eval_quoted(module, quoted, binding, opts) when is_atom(module) and is_list(binding) and is_list(opts) do - assert_not_compiled!(__ENV__.function, module) + assert_not_closing!(__ENV__.function, module) :elixir_def.reset_last(module) {value, binding, _env, _scope} = @@ -1070,7 +1075,7 @@ defmodule Module do """ @spec make_overridable(module, [definition]) :: :ok def make_overridable(module, tuples) when is_atom(module) and is_list(tuples) do - assert_not_compiled!(__ENV__.function, module) + assert_not_closing!(__ENV__.function, module) func = fn {function_name, arity} = tuple @@ -1282,7 +1287,7 @@ defmodule Module do """ @spec delete_attribute(module, atom) :: term def delete_attribute(module, key) when is_atom(module) and is_atom(key) do - assert_not_compiled!(__ENV__.function, module) + assert_not_closing!(__ENV__.function, module) {set, bag} = data_tables_for(module) case :ets.lookup(set, key) do @@ -1334,7 +1339,7 @@ defmodule Module do @spec register_attribute(module, atom, [{:accumulate, boolean}, {:persist, boolean}]) :: :ok def register_attribute(module, attribute, options) when is_atom(module) and is_atom(attribute) and is_list(options) do - assert_not_compiled!(__ENV__.function, module) + assert_not_closing!(__ENV__.function, module) {set, bag} = data_tables_for(module) if Keyword.get(options, :persist) do @@ -1839,7 +1844,7 @@ defmodule Module do # Used internally by Kernel's @. # This function is private and must be used only internally. def __put_attribute__(module, key, value, line) when is_atom(key) do - assert_not_compiled!(__ENV__.function, module) + assert_not_closing!(__ENV__.function, module) {set, bag} = data_tables_for(module) value = preprocess_attribute(key, value) put_attribute(module, key, value, line, set, bag) @@ -2084,6 +2089,12 @@ defmodule Module do assert_not_compiled_message(function_name_arity, module, extra_msg) end + defp assert_not_closing!(function_name_arity, module, extra_msg \\ "") do + (open?(module) and not_closing?(module)) || + raise ArgumentError, + assert_not_compiled_message(function_name_arity, module, extra_msg) + end + defp assert_not_compiled_message({function_name, arity}, module, extra_msg) do mfa = "Module.#{function_name}/#{arity}" diff --git a/lib/elixir/src/elixir_code_server.erl b/lib/elixir/src/elixir_code_server.erl index b8dfc6f154..46fd5ec933 100644 --- a/lib/elixir/src/elixir_code_server.erl +++ b/lib/elixir/src/elixir_code_server.erl @@ -40,6 +40,9 @@ handle_call({defmodule, Module, Pid, Tuple}, _From, Config) -> handle_call({undefmodule, Ref}, _From, Config) -> {reply, ok, undefmodule(Ref, Config)}; +handle_call({unopenmodule, Ref}, _From, Config) -> + {reply, ok, unopenmodule(Ref, Config)}; + handle_call({acquire, Path}, From, Config) -> Current = Config#elixir_code_server.required, case maps:find(Path, Current) of @@ -155,3 +158,12 @@ undefmodule(Ref, #elixir_code_server{mod_ets=ModEts} = Config) -> error -> Config end. + +unopenmodule(Ref, #elixir_code_server{mod_ets=ModEts} = Config) -> + case maps:find(Ref, ModEts) of + {ok, Mod} -> + ets:update_element(elixir_modules, Mod, {5, false}), + Config; + error -> + Config + end. diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 5121482781..494ff944a1 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -1,5 +1,5 @@ -module(elixir_module). --export([file/1, data_tables/1, is_open/1, delete_definition_attributes/6, +-export([file/1, data_tables/1, is_open/1, is_not_closing/1, delete_definition_attributes/6, compile/4, expand_callback/6, format_error/1, compiler_modules/0, write_cache/3, read_cache/2, next_counter/1]). -include("elixir.hrl"). @@ -29,6 +29,9 @@ data_tables(Module) -> is_open(Module) -> ets:member(elixir_modules, Module). +is_not_closing(Module) -> + ets:lookup_element(elixir_modules, Module, 5). + delete_definition_attributes(#{module := Module}, _, _, _, _, _) -> {DataSet, _} = data_tables(Module), ets:delete(DataSet, doc), @@ -119,10 +122,11 @@ compile(Line, Module, Block, Vars, E) -> }, Binary = elixir_erl:compile(ModuleMap), + elixir_code_server:call({unopenmodule, Ref}), warn_unused_attributes(File, DataSet, DataBag, PersistedAttributes), autoload_module(Module, Binary, CompileOpts, NE), - eval_callbacks(Line, DataBag, after_compile, [NE, Binary], NE), make_module_available(Module, Binary, ModuleMap), + eval_callbacks(Line, DataBag, after_compile, [NE, Binary], NE), {module, Module, Binary, Result} catch ?WITH_STACKTRACE(error, undef, Stacktrace) @@ -290,13 +294,13 @@ build(Line, File, Module) -> Tables = {DataSet, DataBag}, elixir_def:setup(Tables), elixir_locals:setup(Tables), - Tuple = {Module, Tables, Line, File}, + Tuple = {Module, Tables, Line, File, true}, Ref = case elixir_code_server:call({defmodule, Module, self(), Tuple}) of {ok, ModuleRef} -> ModuleRef; - {error, {Module, _, OldLine, OldFile}} -> + {error, {Module, _, OldLine, OldFile, _}} -> ets:delete(DataSet), ets:delete(DataBag), Error = {module_in_definition, Module, OldFile, OldLine}, diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs index ae68821c24..d3f8aac64c 100644 --- a/lib/elixir/test/elixir/module_test.exs +++ b/lib/elixir/test/elixir/module_test.exs @@ -77,6 +77,38 @@ defmodule ModuleTest do end end + test "module raises on write access attempts from __after_compile__/2" do + contents = + quote do + @after_compile __MODULE__ + + def __after_compile__(%Macro.Env{module: module}, bin) when is_binary(bin) do + Module.put_attribute(module, :foo, 42) + end + end + + assert_raise ArgumentError, + "could not call Module.__put_attribute__/4 because the module ModuleTest.Raise is already compiled", + fn -> + Module.create(ModuleTest.Raise, contents, __ENV__) + end + end + + test "module supports read access to module from __after_compile__/2" do + contents = + quote do + @after_compile __MODULE__ + + @foo 42 + + def __after_compile__(%Macro.Env{module: module}, bin) when is_binary(bin) do + Module.get_attribute(module, :foo) + end + end + + Module.create(ModuleTest.NoRaise, contents, __ENV__) + end + test "in memory modules are tagged as so" do assert :code.which(__MODULE__) == '' end