* We have created a main named table that keeps all module information * We have reduced the amount of tables by storing internal, docs and attributes altogether and using table looks for retriving the relevant information * At this moment, creating a module in Elixir defines three tables, one for data, another for functions and another for clauses
973 lines
27 KiB
Elixir
973 lines
27 KiB
Elixir
defmodule Module do
|
|
@moduledoc ~S'''
|
|
This module provides many functions to deal with modules during
|
|
compilation time. It allows a developer to dynamically attach
|
|
documentation, add, delete and register attributes and so forth.
|
|
|
|
After a module is compiled, using many of the functions in
|
|
this module will raise errors, since it is out of their scope
|
|
to inspect runtime data. Most of the runtime data can be inspected
|
|
via the `__info__(attr)` function attached to each compiled module.
|
|
|
|
## Module attributes
|
|
|
|
Each module can be decorated with one or more attributes. The following ones
|
|
are currently defined by Elixir:
|
|
|
|
* `@after_compile`
|
|
|
|
A hook that will be invoked right after the current module is compiled.
|
|
|
|
Accepts a module or a tuple `{<module>, <function atom>}`. 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`.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@after_compile __MODULE__
|
|
|
|
def __after_compile__(env, _bytecode) do
|
|
IO.inspect env
|
|
end
|
|
end
|
|
|
|
* `@before_compile`
|
|
|
|
A hook that will be invoked before the module is compiled.
|
|
|
|
Accepts a module or a tuple `{<module>, <function/macro atom>}`. The
|
|
function/macro must take one argument: the module environment. If it's a
|
|
macro, its returned value will be injected at the end of the module definition
|
|
before the compilation starts.
|
|
|
|
When just a module is provided, the function/macro is assumed to be
|
|
`__before_compile__/1`.
|
|
|
|
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).
|
|
|
|
### Example
|
|
|
|
defmodule A do
|
|
defmacro __before_compile__(_env) do
|
|
quote do
|
|
def hello, do: "world"
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule B do
|
|
@before_compile A
|
|
end
|
|
|
|
* `@behaviour` (notice the British spelling)
|
|
|
|
Specify an OTP or user-defined behaviour.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@behaviour gen_event
|
|
|
|
# ...
|
|
end
|
|
|
|
* `@compile`
|
|
|
|
Define options for module compilation that are passed to the Erlang
|
|
compiler.
|
|
|
|
Accepts an atom, a tuple, or a list of atoms and tuples.
|
|
|
|
See http://www.erlang.org/doc/man/compile.html for the list of supported
|
|
options.
|
|
|
|
Several uses of `@compile` will accumulate instead of overriding
|
|
previous ones.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@compile {:inline, myfun: 1}
|
|
|
|
def myfun(arg) do
|
|
to_string(arg)
|
|
end
|
|
end
|
|
|
|
* `@doc`
|
|
|
|
Provide documentation for the function or macro that follows the
|
|
attribute.
|
|
|
|
Accepts a string (often a heredoc) or `false` where `@doc false` will
|
|
make the function/macro invisible to the documentation extraction tools
|
|
like ExDoc.
|
|
|
|
Can be invoked more than once.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@doc "Hello world"
|
|
def hello do
|
|
"world"
|
|
end
|
|
|
|
@doc """
|
|
Sum.
|
|
"""
|
|
def sum(a, b) do
|
|
a + b
|
|
end
|
|
end
|
|
|
|
* `@file`
|
|
|
|
Change the filename used in stacktraces for the function or macro that
|
|
follows the attribute.
|
|
|
|
Accepts a string. Can be used more than once.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@doc "Hello world"
|
|
@file "hello.ex"
|
|
def hello do
|
|
"world"
|
|
end
|
|
end
|
|
|
|
* `@moduledoc`
|
|
|
|
Provide documentation for the current module.
|
|
|
|
Accepts a string (which is often a heredoc) or `false` where
|
|
`@moduledoc false` will make the module invisible to the
|
|
documentation extraction tools like ExDoc.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@moduledoc """
|
|
A very useful module
|
|
"""
|
|
end
|
|
|
|
|
|
* `@on_definition`
|
|
|
|
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 tuple `{<module>, <function atom>}`. The function
|
|
must take 6 arguments:
|
|
|
|
- the module environment
|
|
- kind: `:def`, `:defp`, `:defmacro`, or `:defmacrop`
|
|
- function/macro name
|
|
- list of expanded arguments
|
|
- list of expanded guards
|
|
- expanded function body
|
|
|
|
Note the hook receives the expanded arguments and it is invoked before
|
|
the function is stored in the module. So `Module.defines?/2` will return
|
|
false for the first clause of every function.
|
|
|
|
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 because the hook is invoked inside the context
|
|
of the function (and nested function definitions are not allowed in
|
|
Elixir).
|
|
|
|
When just a module is provided, the function is assumed to be
|
|
`__on_definition__/6`.
|
|
|
|
### Example
|
|
|
|
defmodule H 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 M do
|
|
@on_definition {H, :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 a function atom of a function in the current module. The function
|
|
must have arity 0 (no arguments) and has to return `:ok`, otherwise the
|
|
loading of the module will be aborted.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@on_load :load_check
|
|
|
|
def load_check do
|
|
if some_condition() do
|
|
:ok
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def some_condition do
|
|
false
|
|
end
|
|
end
|
|
|
|
* `@vsn`
|
|
|
|
Specify the module version. Accepts any valid Elixir value.
|
|
|
|
### Example
|
|
|
|
defmodule M do
|
|
@vsn "1.0"
|
|
end
|
|
|
|
* `@external_resource`
|
|
|
|
Specify an external resource to the current module.
|
|
|
|
Many times a module embeds information from an external file. This
|
|
attribute allows the module to annotate which external resources
|
|
have been used.
|
|
|
|
Tools like Mix may use this information to ensure the module is
|
|
recompiled in case any of the external resources change.
|
|
|
|
The following attributes are part of typespecs and are also reserved by
|
|
Elixir (see `Kernel.Typespec` for more information about typespecs):
|
|
|
|
* `@type` - defines a type to be used in `@spec`
|
|
* `@typep` - defines a private type to be used in `@spec`
|
|
* `@opaque` - defines an opaque type to be used in `@spec`
|
|
* `@spec` - provides a specification for a function
|
|
* `@callback` - provides a specification for the behaviour callback
|
|
|
|
In addition to the built-in attributes outlined above, custom attributes may
|
|
also be added. A custom attribute is any valid identifier prefixed with an
|
|
`@` and followed by a valid Elixir value:
|
|
|
|
defmodule M do
|
|
@custom_attr [some: "stuff"]
|
|
end
|
|
|
|
For more advanced options available when defining custom attributes, see
|
|
`register_attribute/3`.
|
|
|
|
## Runtime information about a module
|
|
|
|
It is possible to query a module at runtime to find out which functions and
|
|
macros it defines, extract its docstrings, etc. See `__info__/1`.
|
|
'''
|
|
|
|
@doc """
|
|
Provides runtime information about functions and macros defined by the
|
|
module, enables docstring extraction, etc.
|
|
|
|
Each module gets an `__info__/1` function when it's compiled. The function
|
|
takes one of the following atoms:
|
|
|
|
* `:functions` - keyword list of public functions along with their arities
|
|
|
|
* `:macros` - keyword list of public macros along with their arities
|
|
|
|
* `:module` - module name (`Module == Module.__info__(:module)`)
|
|
|
|
In addition to the above, you may also pass to `__info__/1` any atom supported
|
|
by Erlang's `module_info` function which also gets defined for each compiled
|
|
module. See http://erlang.org/doc/reference_manual/modules.html#id69430 for
|
|
more information.
|
|
"""
|
|
def __info__(kind)
|
|
|
|
@doc """
|
|
Check if a module is open, i.e. it is currently being defined
|
|
and its attributes and functions can be modified.
|
|
"""
|
|
def open?(module) do
|
|
:elixir_module.is_open(module)
|
|
end
|
|
|
|
@doc """
|
|
Evaluates the quoted contents in the given module's context.
|
|
|
|
A list of environment options can also be given as argument.
|
|
See `Code.eval_string/3` for more information.
|
|
|
|
Raises an error if the module was already compiled.
|
|
|
|
## Examples
|
|
|
|
defmodule Foo do
|
|
contents = quote do: (def sum(a, b), do: a + b)
|
|
Module.eval_quoted __MODULE__, contents
|
|
end
|
|
|
|
Foo.sum(1, 2) #=> 3
|
|
|
|
For convenience, you can my pass `__ENV__` as argument and
|
|
all options will be automatically extracted from the environment:
|
|
|
|
defmodule Foo do
|
|
contents = quote do: (def sum(a, b), do: a + b)
|
|
Module.eval_quoted __MODULE__, contents, [], __ENV__
|
|
end
|
|
|
|
Foo.sum(1, 2) #=> 3
|
|
|
|
"""
|
|
def eval_quoted(module, quoted, binding \\ [], opts \\ [])
|
|
|
|
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts) do
|
|
eval_quoted(env.module, quoted, binding, Keyword.merge(Map.to_list(env), opts))
|
|
end
|
|
|
|
def eval_quoted(module, quoted, binding, %Macro.Env{} = env) do
|
|
eval_quoted(module, quoted, binding, Map.to_list(env))
|
|
end
|
|
|
|
def eval_quoted(module, quoted, binding, opts) do
|
|
assert_not_compiled!(:eval_quoted, module)
|
|
:elixir_def.reset_last(module)
|
|
{value, binding, _env, _scope} =
|
|
:elixir.eval_quoted quoted, binding, Keyword.put(opts, :module, module)
|
|
{value, binding}
|
|
end
|
|
|
|
@doc """
|
|
Creates a module with the given name and defined by
|
|
the given quoted expressions.
|
|
|
|
The line where the module is defined and its file **must**
|
|
be passed as options.
|
|
|
|
## Examples
|
|
|
|
contents =
|
|
quote do
|
|
def world, do: true
|
|
end
|
|
|
|
Module.create(Hello, contents, Macro.Env.location(__ENV__))
|
|
|
|
Hello.world #=> true
|
|
|
|
## Differences from `defmodule`
|
|
|
|
`Module.create` works similarly to `defmodule` and
|
|
return the same results. While one could also use
|
|
`defmodule` to define modules dynamically, this
|
|
function is preferred when the module body is given
|
|
by a quoted expression.
|
|
|
|
Another important distinction is that `Module.create`
|
|
allows you to control the environment variables used
|
|
when defining the module, while `defmodule` automatically
|
|
shares the same environment.
|
|
"""
|
|
def create(module, quoted, opts)
|
|
|
|
def create(module, quoted, %Macro.Env{} = env) do
|
|
create(module, quoted, Map.to_list(env))
|
|
end
|
|
|
|
def create(module, quoted, opts) when is_atom(module) and is_list(opts) do
|
|
unless Keyword.has_key?(opts, :file) do
|
|
raise ArgumentError, "expected :file to be given as option"
|
|
end
|
|
:elixir_module.compile(module, quoted, [], :elixir.env_for_eval(opts))
|
|
end
|
|
|
|
@doc """
|
|
Concatenates a list of aliases and returns a new alias.
|
|
|
|
## Examples
|
|
|
|
iex> Module.concat([Foo, Bar])
|
|
Foo.Bar
|
|
|
|
iex> Module.concat([Foo, "Bar"])
|
|
Foo.Bar
|
|
|
|
"""
|
|
@spec concat([binary | atom]) :: atom
|
|
def concat(list) when is_list(list) do
|
|
:elixir_aliases.concat(list)
|
|
end
|
|
|
|
@doc """
|
|
Concatenates two aliases and returns a new alias.
|
|
|
|
## Examples
|
|
|
|
iex> Module.concat(Foo, Bar)
|
|
Foo.Bar
|
|
|
|
iex> Module.concat(Foo, "Bar")
|
|
Foo.Bar
|
|
|
|
"""
|
|
@spec concat(binary | atom, binary | atom) :: atom
|
|
def concat(left, right) do
|
|
:elixir_aliases.concat([left, right])
|
|
end
|
|
|
|
@doc """
|
|
Concatenates a list of aliases and returns a new alias only
|
|
if the alias was already referenced. If the alias was not
|
|
referenced yet, fails with `ArgumentError`.
|
|
It handles char lists, binaries and atoms.
|
|
|
|
## Examples
|
|
|
|
iex> Module.safe_concat([Unknown, Module])
|
|
** (ArgumentError) argument error
|
|
|
|
iex> Module.safe_concat([List, Chars])
|
|
List.Chars
|
|
|
|
"""
|
|
@spec safe_concat([binary | atom]) :: atom | no_return
|
|
def safe_concat(list) when is_list(list) do
|
|
:elixir_aliases.safe_concat(list)
|
|
end
|
|
|
|
@doc """
|
|
Concatenates two aliases and returns a new alias only
|
|
if the alias was already referenced. If the alias was not
|
|
referenced yet, fails with `ArgumentError`.
|
|
It handles char lists, binaries and atoms.
|
|
|
|
## Examples
|
|
|
|
iex> Module.safe_concat(Unknown, Module)
|
|
** (ArgumentError) argument error
|
|
|
|
iex> Module.safe_concat(List, Chars)
|
|
List.Chars
|
|
|
|
"""
|
|
@spec safe_concat(binary | atom, binary | atom) :: atom | no_return
|
|
def safe_concat(left, right) do
|
|
:elixir_aliases.safe_concat([left, right])
|
|
end
|
|
|
|
@doc """
|
|
Attaches documentation to a given function or type. It expects
|
|
the module the function/type belongs to, the line (a non negative
|
|
integer), the kind (`def` or `defmacro`), a tuple representing
|
|
the function and its arity, the function signature (the signature
|
|
should be omitted for types) and the documentation, which should
|
|
be either a binary or a boolean.
|
|
|
|
## Examples
|
|
|
|
defmodule MyModule do
|
|
Module.add_doc(__MODULE__, __ENV__.line + 1, :def, {:version, 0}, [], "Manually added docs")
|
|
def version, do: 1
|
|
end
|
|
|
|
"""
|
|
def add_doc(module, line, kind, tuple, signature \\ [], doc)
|
|
|
|
def add_doc(_module, _line, kind, _tuple, _signature, doc) when kind in [:defp, :defmacrop, :typep] do
|
|
if doc, do: {:error, :private_doc}, else: :ok
|
|
end
|
|
|
|
def add_doc(module, line, kind, tuple, signature, doc) when
|
|
kind in [:def, :defmacro, :type, :opaque] and (is_binary(doc) or is_boolean(doc) or doc == nil) do
|
|
assert_not_compiled!(:add_doc, module)
|
|
table = data_table_for(module)
|
|
|
|
{signature, _} = :lists.mapfoldl fn(x, acc) ->
|
|
{simplify_signature(x, acc), acc + 1}
|
|
end, 1, signature
|
|
|
|
case :ets.lookup(table, {:doc, tuple}) do
|
|
[] ->
|
|
:ets.insert(table, {{:doc, tuple}, line, kind, signature, doc})
|
|
:ok
|
|
[{doc_tuple, line, _old_kind, old_sign, old_doc}] ->
|
|
:ets.insert(table, {
|
|
doc_tuple,
|
|
line,
|
|
kind,
|
|
merge_signatures(old_sign, signature, 1),
|
|
if(is_nil(doc), do: old_doc, else: doc)
|
|
})
|
|
:ok
|
|
end
|
|
end
|
|
|
|
# Simplify signatures to be stored in docs
|
|
|
|
defp simplify_signature({:\\, _, [left, right ]}, i) do
|
|
{:\\, [], [simplify_signature(left, i), right]}
|
|
end
|
|
|
|
defp simplify_signature({:%, _, [left, _]}, _i) when is_atom(left) do
|
|
last = List.last(String.split(Atom.to_string(left), "."))
|
|
atom = String.to_atom(downcase(last))
|
|
{atom, [], nil}
|
|
end
|
|
|
|
defp simplify_signature({:=, _, [_, right]}, i) do
|
|
simplify_signature(right, i)
|
|
end
|
|
|
|
defp simplify_signature({var, _, atom}, _i) when is_atom(atom) do
|
|
case Atom.to_string(var) do
|
|
"_" <> rest -> {String.to_atom(rest), [], Elixir}
|
|
_ -> {var, [], nil}
|
|
end
|
|
end
|
|
|
|
defp simplify_signature(other, i) when is_integer(other), do: {:"int#{i}", [], Elixir}
|
|
defp simplify_signature(other, i) when is_boolean(other), do: {:"bool#{i}", [], Elixir}
|
|
defp simplify_signature(other, i) when is_atom(other), do: {:"atom#{i}", [], Elixir}
|
|
defp simplify_signature(other, i) when is_list(other), do: {:"list#{i}", [], Elixir}
|
|
defp simplify_signature(other, i) when is_float(other), do: {:"float#{i}", [], Elixir}
|
|
defp simplify_signature(other, i) when is_binary(other), do: {:"binary#{i}", [], Elixir}
|
|
defp simplify_signature(_, i), do: {:"arg#{i}", [], Elixir}
|
|
|
|
defp downcase(<<c :: utf8, rest :: binary>>) when c >= ?A and c <= ?Z do
|
|
<<c + 32 :: utf8, downcase(rest) :: binary>>
|
|
end
|
|
|
|
defp downcase(<<c, rest :: binary>>) do
|
|
<<c, downcase(rest) :: binary>>
|
|
end
|
|
|
|
defp downcase(<<>>) do
|
|
<<>>
|
|
end
|
|
|
|
# Merge
|
|
|
|
defp merge_signatures([h1|t1], [h2|t2], i) do
|
|
[merge_signature(h1, h2, i)|merge_signatures(t1, t2, i + 1)]
|
|
end
|
|
|
|
defp merge_signatures([], [], _) do
|
|
[]
|
|
end
|
|
|
|
defp merge_signature({:\\, line, [left, right]}, newer, i) do
|
|
{:\\, line, [merge_signature(left, newer, i), right]}
|
|
end
|
|
|
|
defp merge_signature(older, {:\\, _, [left, _]}, i) do
|
|
merge_signature(older, left, i)
|
|
end
|
|
|
|
# The older signature, when given, always have higher precedence
|
|
defp merge_signature({_, _, nil} = older, _newer, _), do: older
|
|
defp merge_signature(_older, {_, _, nil} = newer, _), do: newer
|
|
|
|
# Both are a guess, so check if they are the same guess
|
|
defp merge_signature({var, _, _} = older, {var, _, _}, _), do: older
|
|
|
|
# Otherwise, returns a generic guess
|
|
defp merge_signature({_, line, _}, _newer, i), do: {:"arg#{i}", line, Elixir}
|
|
|
|
@doc """
|
|
Checks if the module defines the given function or macro.
|
|
Use `defines?/3` to assert for a specific type.
|
|
|
|
## Examples
|
|
|
|
defmodule Example do
|
|
Module.defines? __MODULE__, {:version, 0} #=> false
|
|
def version, do: 1
|
|
Module.defines? __MODULE__, {:version, 0} #=> true
|
|
end
|
|
|
|
"""
|
|
def defines?(module, tuple) when is_tuple(tuple) do
|
|
assert_not_compiled!(:defines?, module)
|
|
table = defs_table_for(module)
|
|
:ets.lookup(table, tuple) != []
|
|
end
|
|
|
|
@doc """
|
|
Checks if the module defines a function or macro of the
|
|
given `kind`. `kind` can be any of `:def`, `:defp`,
|
|
`:defmacro` or `:defmacrop`.
|
|
|
|
## Examples
|
|
|
|
defmodule Example do
|
|
Module.defines? __MODULE__, {:version, 0}, :defp #=> false
|
|
def version, do: 1
|
|
Module.defines? __MODULE__, {:version, 0}, :defp #=> false
|
|
end
|
|
|
|
"""
|
|
def defines?(module, tuple, kind) do
|
|
assert_not_compiled!(:defines?, module)
|
|
table = defs_table_for(module)
|
|
case :ets.lookup(table, tuple) do
|
|
[{_, ^kind, _, _, _, _, _}] -> true
|
|
_ -> false
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Return all functions defined in `module`.
|
|
|
|
## Examples
|
|
|
|
defmodule Example do
|
|
def version, do: 1
|
|
Module.definitions_in __MODULE__ #=> [{:version,0}]
|
|
end
|
|
|
|
"""
|
|
def definitions_in(module) do
|
|
assert_not_compiled!(:definitions_in, module)
|
|
table = defs_table_for(module)
|
|
:lists.concat :ets.match(table, {:'$1', :_, :_, :_, :_, :_, :_})
|
|
end
|
|
|
|
@doc """
|
|
Returns all functions defined in `module`, according
|
|
to its kind.
|
|
|
|
## Examples
|
|
|
|
defmodule Example do
|
|
def version, do: 1
|
|
Module.definitions_in __MODULE__, :def #=> [{:version,0}]
|
|
Module.definitions_in __MODULE__, :defp #=> []
|
|
end
|
|
|
|
"""
|
|
def definitions_in(module, kind) do
|
|
assert_not_compiled!(:definitions_in, module)
|
|
table = defs_table_for(module)
|
|
:lists.concat :ets.match(table, {:'$1', kind, :_, :_, :_, :_, :_})
|
|
end
|
|
|
|
@doc """
|
|
Makes the given functions in `module` overridable.
|
|
An overridable function is lazily defined, allowing a
|
|
developer to customize it. See `Kernel.defoverridable/1` for
|
|
more information and documentation.
|
|
"""
|
|
def make_overridable(module, tuples) do
|
|
assert_not_compiled!(:make_overridable, module)
|
|
|
|
for tuple <- tuples do
|
|
case :elixir_def.lookup_definition(module, tuple) do
|
|
false ->
|
|
{name, arity} = tuple
|
|
raise "Cannot make function #{name}/#{arity} overridable because it was not defined"
|
|
clause ->
|
|
:elixir_def.delete_definition(module, tuple)
|
|
|
|
neighbours = if loaded?(Module.LocalsTracker) do
|
|
Module.LocalsTracker.yank(module, tuple)
|
|
else
|
|
[]
|
|
end
|
|
|
|
old = :elixir_def_overridable.overridable(module)
|
|
merged = :orddict.update(tuple, fn({count, _, _, _}) ->
|
|
{count + 1, clause, neighbours, false}
|
|
end, {1, clause, neighbours, false}, old)
|
|
:elixir_def_overridable.overridable(module, merged)
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns `true` if `tuple` in `module` is marked as overridable.
|
|
"""
|
|
def overridable?(module, tuple) do
|
|
!!List.keyfind(:elixir_def_overridable.overridable(module), tuple, 0)
|
|
end
|
|
|
|
@doc """
|
|
Puts an Erlang attribute to the given module with the given
|
|
key and value. The semantics of putting the attribute depends
|
|
if the attribute was registered or not via `register_attribute/3`.
|
|
|
|
## Examples
|
|
|
|
defmodule MyModule do
|
|
Module.put_attribute __MODULE__, :custom_threshold_for_lib, 10
|
|
end
|
|
|
|
"""
|
|
def put_attribute(module, key, value) when is_atom(key) do
|
|
assert_not_compiled!(:put_attribute, module)
|
|
table = data_table_for(module)
|
|
value = normalize_attribute(key, value)
|
|
acc = :ets.lookup_element(table, {:elixir, :acc_attributes}, 2)
|
|
|
|
new =
|
|
if :lists.member(key, acc) do
|
|
case :ets.lookup(table, key) do
|
|
[{^key, old}] -> [value|old]
|
|
[] -> [value]
|
|
end
|
|
else
|
|
value
|
|
end
|
|
|
|
:ets.insert(table, {key, new})
|
|
end
|
|
|
|
@doc """
|
|
Gets the given attribute from a module. If the attribute
|
|
was marked with `accumulate` with `Module.register_attribute/3`,
|
|
a list is always returned.
|
|
|
|
The `@` macro compiles to a call to this function. For example,
|
|
the following code:
|
|
|
|
@foo
|
|
|
|
Expands close to:
|
|
|
|
Module.get_attribute(__MODULE__, :foo)
|
|
|
|
## Examples
|
|
|
|
defmodule Foo do
|
|
Module.put_attribute __MODULE__, :value, 1
|
|
Module.get_attribute __MODULE__, :value #=> 1
|
|
|
|
Module.register_attribute __MODULE__, :value, accumulate: true
|
|
Module.put_attribute __MODULE__, :value, 1
|
|
Module.get_attribute __MODULE__, :value #=> [1]
|
|
end
|
|
|
|
"""
|
|
@spec get_attribute(atom, atom) :: term
|
|
def get_attribute(module, key) do
|
|
get_attribute(module, key, nil)
|
|
end
|
|
|
|
@doc """
|
|
Deletes all attributes that match the given key.
|
|
|
|
## Examples
|
|
|
|
defmodule MyModule do
|
|
Module.put_attribute __MODULE__, :custom_threshold_for_lib, 10
|
|
Module.delete_attribute __MODULE__, :custom_threshold_for_lib
|
|
end
|
|
|
|
"""
|
|
@spec delete_attribute(atom, atom) :: :ok
|
|
def delete_attribute(module, key) when is_atom(key) do
|
|
assert_not_compiled!(:delete_attribute, module)
|
|
table = data_table_for(module)
|
|
:ets.delete(table, key)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Registers an attribute. By registering an attribute, a developer
|
|
is able to customize how Elixir will store and accumulate the
|
|
attribute values.
|
|
|
|
## Options
|
|
|
|
When registering an attribute, two options can be given:
|
|
|
|
* `:accumulate` - several calls to the same attribute will
|
|
accumulate instead of override the previous one. New attributes
|
|
are always added to the top of the accumulated list.
|
|
|
|
* `:persist` - the attribute will be persisted in the Erlang
|
|
Abstract Format. Useful when interfacing with Erlang libraries.
|
|
|
|
By default, both options are `false`.
|
|
|
|
## Examples
|
|
|
|
defmodule MyModule do
|
|
Module.register_attribute __MODULE__,
|
|
:custom_threshold_for_lib,
|
|
accumulate: true, persist: false
|
|
|
|
@custom_threshold_for_lib 10
|
|
@custom_threshold_for_lib 20
|
|
@custom_threshold_for_lib #=> [20, 10]
|
|
end
|
|
|
|
"""
|
|
def register_attribute(module, new, opts) when is_atom(new) do
|
|
assert_not_compiled!(:register_attribute, module)
|
|
table = data_table_for(module)
|
|
|
|
if Keyword.get(opts, :persist) do
|
|
old = :ets.lookup_element(table, {:elixir, :persisted_attributes}, 2)
|
|
:ets.insert(table, {{:elixir, :persisted_attributes}, [new|old]})
|
|
end
|
|
|
|
if Keyword.get(opts, :accumulate) do
|
|
old = :ets.lookup_element(table, {:elixir, :acc_attributes}, 2)
|
|
:ets.insert(table, {{:elixir, :acc_attributes}, [new|old]})
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Split the given module name into binary parts.
|
|
|
|
## Examples
|
|
|
|
Module.split Very.Long.Module.Name.And.Even.Longer
|
|
#=> ["Very", "Long", "Module", "Name", "And", "Even", "Longer"]
|
|
|
|
"""
|
|
def split(module) do
|
|
tl(String.split(String.Chars.to_string(module), "."))
|
|
end
|
|
|
|
@doc false
|
|
# Used internally to compile documentation. This function
|
|
# is private and must be used only internally.
|
|
def compile_doc(env, kind, name, args, _guards, _body) do
|
|
module = env.module
|
|
line = env.line
|
|
arity = length(args)
|
|
pair = {name, arity}
|
|
doc = get_attribute(module, :doc)
|
|
|
|
case add_doc(module, line, kind, pair, args, doc) do
|
|
:ok ->
|
|
:ok
|
|
{:error, :private_doc} ->
|
|
:elixir_errors.warn line, env.file,
|
|
"function #{name}/#{arity} is private, " <>
|
|
"@doc's are always discarded for private functions"
|
|
end
|
|
|
|
delete_attribute(module, :doc)
|
|
end
|
|
|
|
@doc false
|
|
# Used internally to compile types. This function
|
|
# is private and must be used only internally.
|
|
def store_typespec(module, key, value) when is_atom(key) do
|
|
assert_not_compiled!(:put_attribute, module)
|
|
table = data_table_for(module)
|
|
|
|
new =
|
|
case :ets.lookup(table, key) do
|
|
[{^key, old}] -> [value|old]
|
|
[] -> [value]
|
|
end
|
|
|
|
:ets.insert(table, {key, new})
|
|
end
|
|
|
|
@doc false
|
|
def get_attribute(module, key, warn) when is_atom(key) and (is_list(warn) or is_nil(warn)) do
|
|
assert_not_compiled!(:get_attribute, module)
|
|
table = data_table_for(module)
|
|
|
|
case :ets.lookup(table, key) do
|
|
[{^key, val}] -> val
|
|
[] ->
|
|
acc = :ets.lookup_element(table, {:elixir, :acc_attributes}, 2)
|
|
|
|
cond do
|
|
:lists.member(key, acc) ->
|
|
[]
|
|
is_list(warn) ->
|
|
:elixir_errors.warn warn_info(warn), "undefined module attribute @#{key}, " <>
|
|
"please remove access to @#{key} or explicitly set it to nil before access"
|
|
nil
|
|
true ->
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
defp warn_info([entry|_]) do
|
|
opts = elem(entry, tuple_size(entry) - 1)
|
|
Exception.format_file_line(Keyword.get(opts, :file), Keyword.get(opts, :line)) <> " "
|
|
end
|
|
|
|
defp warn_info([]) do
|
|
""
|
|
end
|
|
|
|
## Helpers
|
|
|
|
defp normalize_attribute(:on_load, atom) when is_atom(atom) do
|
|
{atom, 0}
|
|
end
|
|
|
|
defp normalize_attribute(:behaviour, atom) when is_atom(atom) do
|
|
# Attempt to compile behaviour but ignore failure (will warn later)
|
|
_ = Code.ensure_compiled(atom)
|
|
atom
|
|
end
|
|
|
|
defp normalize_attribute(:file, file) when is_binary(file) do
|
|
file
|
|
end
|
|
|
|
defp normalize_attribute(key, atom) when is_atom(atom) and
|
|
key in [:before_compile, :after_compile, :on_definition] do
|
|
{atom, :"__#{key}__"}
|
|
end
|
|
|
|
defp normalize_attribute(key, _value) when key in [:type, :typep, :export_type, :opaque, :callback] do
|
|
raise ArgumentError, "attributes type, typep, export_type, opaque and callback " <>
|
|
"must be set via Kernel.Typespec"
|
|
end
|
|
|
|
defp normalize_attribute(_key, value) do
|
|
value
|
|
end
|
|
|
|
defp data_table_for(module) do
|
|
:elixir_module.data_table(module)
|
|
end
|
|
|
|
defp defs_table_for(module) do
|
|
:elixir_module.defs_table(module)
|
|
end
|
|
|
|
defp assert_not_compiled!(fun, module) do
|
|
open?(module) ||
|
|
raise ArgumentError,
|
|
"could not call #{fun} on module #{inspect module} because it was already compiled"
|
|
end
|
|
|
|
defp loaded?(module), do: is_tuple :code.is_loaded(module)
|
|
end
|