summaryrefslogtreecommitdiff
path: root/lib/elixir
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@dashbit.co>2022-07-20 16:32:54 +0200
committerJosé Valim <jose.valim@dashbit.co>2022-07-20 16:34:15 +0200
commit4056be1d4f3a5cc16f97b937d438f07f2a062fd1 (patch)
treeab37a81d2267aae3a2ec63d138a66d4c7e74af1e /lib/elixir
parent85d5dc5793543292ef9d3b0b5804744f4c1818cb (diff)
downloadelixir-4056be1d4f3a5cc16f97b937d438f07f2a062fd1.tar.gz
Add @after_verify callback
@after_verify hooks are invoked right after the current module is verified for undefined functions, deprecations, etc. A module is always verified after it is compiled. In Mix projects, a module is also verified when any of its runtime dependencies change. Therefore this is useful to perform verification of the current module while avoiding compile-time dependencies. Here are some sample use cases: * Ecto can use this validate associations consistently and effectively * Phoenix can use this to verify routes * Surface can use this to verify component attributes
Diffstat (limited to 'lib/elixir')
-rw-r--r--lib/elixir/lib/code.ex2
-rw-r--r--lib/elixir/lib/module.ex177
-rw-r--r--lib/elixir/lib/module/parallel_checker.ex91
-rw-r--r--lib/elixir/src/elixir_module.erl4
-rw-r--r--lib/elixir/test/elixir/module/types/integration_test.exs23
-rw-r--r--lib/elixir/test/elixir/module_test.exs12
6 files changed, 183 insertions, 126 deletions
diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex
index 767cd3ec4..8491e059c 100644
--- a/lib/elixir/lib/code.ex
+++ b/lib/elixir/lib/code.ex
@@ -152,7 +152,7 @@ defmodule Code do
of keys to traverse in the application environment and `return` is either
`{:ok, value}` or `:error`.
- * `{:on_module, bytecode, :none}` - (since v1.11.0) traced whenever a module
+ * `{:on_module, bytecode, _ignore}` - (since v1.11.0) traced whenever a module
is defined. This is equivalent to the `@after_compile` callback and invoked
after any `@after_compile` in the given module. The third element is currently
`:none` but it may provide more metadata in the future. It is best to ignore
diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex
index 1d807de5b..92c4d801f 100644
--- a/lib/elixir/lib/module.ex
+++ b/lib/elixir/lib/module.ex
@@ -22,6 +22,12 @@ defmodule Module do
Accepts a module or a `{module, function_name}`. See the "Compile callbacks"
section below.
+ ### `@after_verify` (since v1.14.0)
+
+ A hook that will be invoked right after the current module is verified for
+ undefined functions, deprecations, etc. Accepts a module or a `{module, function_name}`.
+ See the "Compile callbacks" section below.
+
### `@before_compile`
A hook that will be invoked before the module is compiled.
@@ -128,7 +134,7 @@ defmodule Module do
Multiple uses of `@compile` will accumulate instead of overriding
previous ones. See the "Compile options" section below.
- ### `@deprecated` (since 1.6.0)
+ ### `@deprecated` (since v1.6.0)
Provides the deprecation reason for a function. For example:
@@ -238,8 +244,7 @@ defmodule Module do
end
end
- For the list of supported warnings, see
- [`:dialyzer` module](`:dialyzer`).
+ For the list of supported warnings, see [`:dialyzer` module](`:dialyzer`).
Multiple uses of `@dialyzer` will accumulate instead of overriding
previous ones.
@@ -300,17 +305,59 @@ defmodule Module do
A hook that will be invoked when each function or macro in the current
module is defined. Useful when annotating functions.
- Accepts a module or a `{module, function_name}` tuple. See the
- "Compile callbacks" section below.
+ Accepts a module or a `{module, function_name}` tuple. The function
+ must take 6 arguments:
+
+ * the module environment
+ * the kind of the function/macro: `:def`, `:defp`, `:defmacro`, or `:defmacrop`
+ * the function/macro name
+ * the list of quoted arguments
+ * the list of quoted guards
+ * the quoted function body
+
+ If the function/macro being defined has multiple clauses, the hook will
+ be called for each clause.
+
+ Unlike other hooks, `@on_definition` will only invoke functions and
+ never macros. This is to avoid `@on_definition` callbacks from
+ redefining functions that have just been defined in favor of more
+ explicit approaches.
+
+ When just a module is provided, the function is assumed to be
+ `__on_definition__/6`.
+
+ #### Example
+
+ defmodule Hooks do
+ def on_def(_env, kind, name, args, guards, body) do
+ IO.puts("Defining #{kind} named #{name} with args:")
+ IO.inspect(args)
+ IO.puts("and guards")
+ IO.inspect(guards)
+ IO.puts("and body")
+ IO.puts(Macro.to_string(body))
+ end
+ end
+
+ defmodule MyModule do
+ @on_definition {Hooks, :on_def}
+
+ def hello(arg) when is_binary(arg) or is_list(arg) do
+ "Hello" <> to_string(arg)
+ end
+
+ def hello(_) do
+ :ok
+ end
+ end
### `@on_load`
A hook that will be invoked whenever the module is loaded.
- Accepts the function name (as an atom) of a function in the current module or
- `{function_name, 0}` tuple where `function_name` is the name of a function in
- the current module. The function must have an arity of 0 (no arguments). If
- the function does not return `:ok`, the loading of the module will be aborted.
+ Accepts the function name (as an atom) of a function in the current module.
+ The function must have an arity of 0 (no arguments). If the function does
+ not return `:ok`, the loading of the module will be aborted.
For example:
defmodule MyModule do
@@ -380,36 +427,14 @@ defmodule Module do
## Compile callbacks
- There are three callbacks that are invoked when functions are defined,
- as well as before and immediately after the module bytecode is generated.
-
- ### `@after_compile`
-
- A hook that will be invoked right after the current module is compiled.
-
- Accepts a module or a `{module, function_name}` tuple. The function
- must take two arguments: the module environment and its bytecode.
- When just a module is provided, the function is assumed to be
- `__after_compile__/2`.
-
- Callbacks will run in the order they are registered.
-
- `Module` functions expecting not yet compiled modules (such as `definitions_in/1`)
- are still available at the time `@after_compile` is invoked.
-
- #### Example
-
- defmodule MyModule do
- @after_compile __MODULE__
-
- def __after_compile__(env, _bytecode) do
- IO.inspect(env)
- end
- end
+ There are three compilation callbacks, invoked in this order:
+ `@before_compile`, `@after_compile`, and `@after_verify`.
+ They are described next.
### `@before_compile`
- A hook that will be invoked before the module is compiled.
+ A hook that will be invoked before the module is compiled. This is
+ often used to change how the current module is being compiled.
Accepts a module or a `{module, function_or_macro_name}` tuple. The
function/macro must take one argument: the module environment. If
@@ -425,9 +450,8 @@ defmodule Module do
callback and it will be made concrete one last time after all callbacks
run.
- *Note*: unlike `@after_compile`, the callback function/macro must
- be placed in a separate module (because when the callback is invoked,
- the current module does not yet exist).
+ *Note*: the callback function/macro must be placed in a separate module
+ (because when the callback is invoked, the current module does not yet exist).
#### Example
@@ -446,53 +470,54 @@ defmodule Module do
B.hello()
#=> "world"
- ### `@on_definition`
+ ### `@after_compile`
- A hook that will be invoked when each function or macro in the current
- module is defined. Useful when annotating functions.
+ A hook that will be invoked right after the current module is compiled.
Accepts a module or a `{module, function_name}` tuple. The function
- must take 6 arguments:
-
- * the module environment
- * the kind of the function/macro: `:def`, `:defp`, `:defmacro`, or `:defmacrop`
- * the function/macro name
- * the list of quoted arguments
- * the list of quoted guards
- * the quoted function body
-
- If the function/macro being defined has multiple clauses, the hook will
- be called for each clause.
+ must take two arguments: the module environment and its bytecode.
+ When just a module is provided, the function is assumed to be
+ `__after_compile__/2`.
- Unlike other hooks, `@on_definition` will only invoke functions and
- never macros. This is to avoid `@on_definition` callbacks from
- redefining functions that have just been defined in favor of more
- explicit approaches.
+ Callbacks will run in the order they are registered.
- When just a module is provided, the function is assumed to be
- `__on_definition__/6`.
+ `Module` functions expecting not yet compiled modules (such as `definitions_in/1`)
+ are still available at the time `@after_compile` is invoked.
#### Example
- defmodule Hooks do
- def on_def(_env, kind, name, args, guards, body) do
- IO.puts("Defining #{kind} named #{name} with args:")
- IO.inspect(args)
- IO.puts("and guards")
- IO.inspect(guards)
- IO.puts("and body")
- IO.puts(Macro.to_string(body))
+ defmodule MyModule do
+ @after_compile __MODULE__
+
+ def __after_compile__(env, _bytecode) do
+ IO.inspect(env)
end
end
- defmodule MyModule do
- @on_definition {Hooks, :on_def}
+ ### `@after_verify`
- def hello(arg) when is_binary(arg) or is_list(arg) do
- "Hello" <> to_string(arg)
- end
+ A hook that will be invoked right after the current module is verified for
+ undefined functions, deprecations, etc. A module is always verified after
+ it is compiled. In Mix projects, a module is also verified when any of its
+ runtime dependencies change. Therefore this is useful to perform verification
+ of the current module while avoiding compile-time dependencies.
- def hello(_) do
+ Accepts a module or a `{module, function_name}` tuple. The function
+ must take one argument: the module name. When just a module is provided,
+ the function is assumed to be `__after_verify__/2`.
+
+ Callbacks will run in the order they are registered.
+
+ `Module` functions expecting not yet compiled modules are no longer available
+ at the time `@after_verify` is invoked.
+
+ #### Example
+
+ defmodule MyModule do
+ @after_verify __MODULE__
+
+ def __after_verify__(module) do
+ IO.inspect(module)
:ok
end
end
@@ -582,6 +607,9 @@ defmodule Module do
after_compile: %{
doc: "A hook that will be invoked right after the current module is compiled."
},
+ after_verify: %{
+ doc: "A hook that will be invoked right after the current module is verified."
+ },
before_compile: %{
doc: "A hook that will be invoked before the module is compiled."
},
@@ -2312,6 +2340,9 @@ defmodule Module do
defp preprocess_attribute(:after_compile, atom) when is_atom(atom),
do: {atom, :__after_compile__}
+ defp preprocess_attribute(:after_verify, atom) when is_atom(atom),
+ do: {atom, :__after_verify__}
+
defp preprocess_attribute(:on_definition, atom) when is_atom(atom),
do: {atom, :__on_definition__}
diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex
index 0530fe718..f2762add3 100644
--- a/lib/elixir/lib/module/parallel_checker.ex
+++ b/lib/elixir/lib/module/parallel_checker.ex
@@ -58,21 +58,19 @@ defmodule Module.ParallelChecker do
receive do
{^ref, :cache, ets} ->
- loaded_info =
+ module_map =
if is_map(info) do
- cache_from_module_map(ets, info)
info
else
- info = File.read!(info)
- cache_from_chunk(ets, module, info)
- info
+ info |> File.read!() |> fetch_module_map!(module)
end
+ cache_from_module_map(ets, module_map)
send(checker, {ref, :cached})
receive do
{^ref, :check} ->
- warnings = check_module(module, loaded_info, {checker, ets})
+ warnings = check_module(module_map, {checker, ets})
send(pid, {__MODULE__, module, warnings})
send(checker, {__MODULE__, :done})
end
@@ -206,35 +204,26 @@ defmodule Module.ParallelChecker do
## Module checking
- defp check_module(module, info, cache) do
- case extract_definitions(module, info) do
- {:ok, module, file, definitions, no_warn_undefined} ->
- Module.Types.warnings(module, file, definitions, no_warn_undefined, cache)
- |> group_warnings()
- |> emit_warnings()
-
- :error ->
- []
- end
- end
+ defp check_module(module_map, cache) do
+ %{module: module, file: file, compile_opts: compile_opts, definitions: definitions} =
+ module_map
- defp extract_definitions(module, module_map) when is_map(module_map) do
no_warn_undefined =
- module_map.compile_opts
+ compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
- {:ok, module, module_map.file, module_map.definitions, no_warn_undefined}
- end
+ warnings =
+ module
+ |> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
+ |> group_warnings()
+ |> emit_warnings()
- defp extract_definitions(module, binary) when is_binary(binary) do
- with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
- {:debug_info_v1, backend, data} <- chunk,
- {:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
- extract_definitions(module, module_map)
- else
- _ -> :error
- end
+ module_map
+ |> Map.get(:after_verify, [])
+ |> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
+
+ warnings
end
defp extract_no_warn_undefined(compile_opts) do
@@ -247,11 +236,8 @@ defmodule Module.ParallelChecker do
defp merge_compiler_no_warn_undefined(no_warn_undefined) do
case Code.get_compiler_option(:no_warn_undefined) do
- :all ->
- :all
-
- list when is_list(list) ->
- no_warn_undefined ++ list
+ :all -> :all
+ list when is_list(list) -> no_warn_undefined ++ list
end
end
@@ -322,14 +308,8 @@ defmodule Module.ParallelChecker do
end
defp cache_from_chunk(ets, module) do
- case :code.get_object_code(module) do
- {^module, binary, _filename} -> cache_from_chunk(ets, module, binary)
- _other -> false
- end
- end
-
- defp cache_from_chunk(ets, module, binary) do
- with {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
+ with {^module, binary, _filename} <- :code.get_object_code(module),
+ {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
cache_chunk(ets, module, contents.exports)
true
@@ -338,16 +318,6 @@ defmodule Module.ParallelChecker do
end
end
- defp cache_from_module_map(ets, map) do
- exports =
- [{{:__info__, 1}, :def}] ++
- behaviour_exports(map) ++
- definitions_to_exports(map.definitions)
-
- deprecated = Map.new(map.deprecated)
- cache_info(ets, map.module, exports, deprecated, :elixir)
- end
-
defp cache_from_info(ets, module) do
if Code.ensure_loaded?(module) do
{mode, exports} = info_exports(module)
@@ -378,6 +348,23 @@ defmodule Module.ParallelChecker do
_ -> %{}
end
+ defp fetch_module_map!(binary, module) when is_binary(binary) do
+ {:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
+ {:debug_info_v1, backend, data} = chunk
+ {:ok, module_map} = backend.debug_info(:elixir_v1, module, data, [])
+ module_map
+ end
+
+ defp cache_from_module_map(ets, map) do
+ exports =
+ [{{:__info__, 1}, :def}] ++
+ behaviour_exports(map) ++
+ definitions_to_exports(map.definitions)
+
+ deprecated = Map.new(map.deprecated)
+ cache_info(ets, map.module, exports, deprecated, :elixir)
+ end
+
defp cache_info(ets, module, exports, deprecated, mode) do
Enum.each(exports, fn {{fun, arity}, kind} ->
reason = Map.get(deprecated, {fun, arity})
diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl
index 2b7084d19..462b8ed73 100644
--- a/lib/elixir/src/elixir_module.erl
+++ b/lib/elixir/src/elixir_module.erl
@@ -131,6 +131,7 @@ compile(Line, Module, Block, Vars, E) ->
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, File, Line),
+ AfterVerify = bag_lookup_element(DataBag, {accumulate, after_verify}, 2),
ModuleMap = #{
struct => get_struct(DataSet),
@@ -141,6 +142,7 @@ compile(Line, Module, Block, Vars, E) ->
attributes => Attributes,
definitions => AllDefinitions,
unreachable => Unreachable,
+ after_verify => AfterVerify,
compile_opts => CompileOpts,
deprecated => get_deprecated(DataBag),
is_behaviour => is_behaviour(DataBag)
@@ -157,6 +159,7 @@ compile(Line, Module, Block, Vars, E) ->
elixir_env:trace({on_module, Binary, none}, E),
warn_unused_attributes(File, DataSet, DataBag, PersistedAttributes),
make_module_available(Module, Binary),
+ (CheckerInfo == undefined) andalso eval_callbacks(Line, DataBag, after_verify, [Module], NE),
{module, Module, Binary, Result}
catch
error:undef:Stacktrace ->
@@ -316,6 +319,7 @@ build(Line, File, Module) ->
% {Key, Value, accumulate, TraceLine}
{after_compile, [], accumulate, []},
+ {after_verify, [], accumulate, []},
{before_compile, [], accumulate, []},
{behaviour, [], accumulate, []},
{compile, [], accumulate, []},
diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs
index 175163b59..e59311784 100644
--- a/lib/elixir/test/elixir/module/types/integration_test.exs
+++ b/lib/elixir/test/elixir/module/types/integration_test.exs
@@ -557,6 +557,29 @@ defmodule Module.Types.IntegrationTest do
end
end
+ describe "after_verify" do
+ test "reports functions" do
+ files = %{
+ "a.ex" => """
+ defmodule A do
+ @after_verify __MODULE__
+
+ def __after_verify__(__MODULE__) do
+ IO.warn "from after_verify", []
+ end
+ end
+ """
+ }
+
+ warning = """
+ warning: from after_verify
+
+ """
+
+ assert_warnings(files, warning)
+ end
+ end
+
describe "deprecated" do
test "reports functions" do
files = %{
diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs
index 547af8993..3c7aff406 100644
--- a/lib/elixir/test/elixir/module_test.exs
+++ b/lib/elixir/test/elixir/module_test.exs
@@ -108,6 +108,18 @@ defmodule ModuleTest do
assert_received 42
end
+ test "supports @after_verify for inlined modules" do
+ defmodule ModuleTest.AfterVerify do
+ @after_verify __MODULE__
+
+ def __after_verify__(ModuleTest.AfterVerify) do
+ send(self(), ModuleTest.AfterVerify)
+ end
+ end
+
+ assert_received ModuleTest.AfterVerify
+ end
+
test "in memory modules are tagged as so" do
assert :code.which(__MODULE__) == ''
end