summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@plataformatec.com.br>2019-08-02 13:35:12 +0200
committerGitHub <noreply@github.com>2019-08-02 13:35:12 +0200
commitb08593b9d1b82e65035abc9bf074ea47dc5817b1 (patch)
tree722433438740cab97e11776390e1552155036cff
parentd82616786567d497fa08f62d4f3dc2a7d2134306 (diff)
downloadelixir-b08593b9d1b82e65035abc9bf074ea47dc5817b1.tar.gz
Add compiler tracing (#9247)
This feature allows IDEs and other tools wanting to perform source code analysis to do so reliably without a need to reimplement Elixir's compiler expansion and without relying on Elixir's private APIs. This commit also adds :parser_options to compiler options, which allows developers to combine both options to retrieve more accurate information, such as columns.
-rw-r--r--lib/elixir/lib/code.ex210
-rw-r--r--lib/elixir/lib/kernel/lexical_tracker.ex6
-rw-r--r--lib/elixir/lib/macro.ex8
-rw-r--r--lib/elixir/lib/macro/env.ex16
-rw-r--r--lib/elixir/lib/module.ex8
-rw-r--r--lib/elixir/lib/module/checker.ex2
-rw-r--r--lib/elixir/src/elixir.erl10
-rw-r--r--lib/elixir/src/elixir_aliases.erl34
-rw-r--r--lib/elixir/src/elixir_compiler.erl12
-rw-r--r--lib/elixir/src/elixir_def.erl10
-rw-r--r--lib/elixir/src/elixir_dispatch.erl31
-rw-r--r--lib/elixir/src/elixir_env.erl43
-rw-r--r--lib/elixir/src/elixir_expand.erl27
-rw-r--r--lib/elixir/src/elixir_import.erl18
-rw-r--r--lib/elixir/src/elixir_lexical.erl117
-rw-r--r--lib/elixir/src/elixir_map.erl4
-rw-r--r--lib/elixir/src/elixir_module.erl19
-rw-r--r--lib/elixir/src/elixir_quote.erl2
-rw-r--r--lib/elixir/test/elixir/code_test.exs8
-rw-r--r--lib/elixir/test/elixir/kernel/lexical_tracker_test.exs22
-rw-r--r--lib/elixir/test/elixir/kernel/parallel_compiler_test.exs4
-rw-r--r--lib/elixir/test/elixir/kernel/tracers_test.exs160
-rw-r--r--lib/elixir/test/elixir/module/checker_test.exs2
-rw-r--r--lib/mix/lib/mix/compilers/elixir.ex2
-rw-r--r--lib/mix/lib/mix/compilers/test.ex2
-rw-r--r--lib/mix/lib/mix/tasks/compile.elixir.ex34
-rw-r--r--lib/mix/test/mix/tasks/compile.elixir_test.exs18
27 files changed, 565 insertions, 264 deletions
diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex
index 5ec4c4e64..3cddcb191 100644
--- a/lib/elixir/lib/code.ex
+++ b/lib/elixir/lib/code.ex
@@ -19,7 +19,8 @@ defmodule Code do
* `eval_file/2` - evaluates the file contents without tracking its name. It
returns the result of the last expression in the file, instead of the modules
- defined in it.
+ defined in it. Evaluated files do not trigger the compilation tracers described
+ in the next section.
In a nutshell, the first must be used when you want to keep track of the files
handled by the system, to avoid the same file from being compiled multiple
@@ -28,6 +29,61 @@ defmodule Code do
`compile_file/2` must be used when you are interested in the modules defined in a
file, without tracking. `eval_file/2` should be used when you are interested in
the result of evaluating the file rather than the modules it defines.
+
+ ## Compilation tracers
+
+ Elixir supports compilation tracers, which allows modules to observe constructs
+ handled by the Elixir compiler when compiling files. A tracer is a module
+ that implements the `trace/2` function. The function receives the event name
+ as first argument and `Macro.Env` as second and it must return `:ok`. It is
+ very important for a tracer to do as little work as possible synchronously
+ and dispatch the bulk of the work to a separate process. **Slow tracers will
+ slow down compilation**.
+
+ You can configure your list of tracers via `put_compiler_option/2`. The
+ following events are available to tracers:
+
+ * `{:import, meta, module, opts}` - traced whenever `module` is imported.
+ `meta` is the import AST metadata and `opts` are the import options.
+
+ * `{:imported_function, meta, module, name, arity}` and
+ `{:imported_macro, meta, module, name, arity}` - traced whenever an
+ imported function or macro is invoked. `meta` is the call AST metadata,
+ `module` is the module the import is from, followed by the `name` and `arity`
+ of the imported function/macro.
+
+ * `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
+ to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
+
+ * `{:alias_expansion, meta, as, alias}` traced whenever there is an alias
+ expansion for a previously defined `alias`, i.e. when the user writes `as`
+ which is expanded to `alias`. `meta` is the alias expansion AST metadata.
+
+ * `{:alias_reference, meta, module}` - traced whenever there is an alias
+ in the code, i.e. whenever the user writes `MyModule.Foo.Bar` in the code,
+ regardless if it was expanded or not.
+
+ * `{:require, meta, module, opts}` - traced whenever `module` is required.
+ `meta` is the require AST metadata and `opts` are the require options.
+
+ * `{:struct_expansion, meta, module}` - traced whenever `module`'s struct
+ is expanded. `meta` is the struct AST metadata.
+
+ * `{:remote_function, meta, module, name, arity}` and
+ `{:remote_macro, meta, module, name, arity}` - traced whenever a remote
+ function or macro is referenced. `meta` is the call AST metadata, `module`
+ is the invoked module, followed by the `name` and `arity`.
+
+ * `{:local_function, meta, module, name, arity}` and
+ `{:local_macro, meta, module, name, arity}` - traced whenever a local
+ function or macro is referenced. `meta` is the call AST metadata, `module`
+ is the invoked module, followed by the `name` and `arity`.
+
+ The `:tracers` compiler option can be combined with the `:parser_options`
+ compiler option to enrich the metadata of the traced events above.
+
+ New events may be added at any time in the future, therefore it is advised
+ for the `trace/2` function to have a "catch-all" clause.
"""
@boolean_compiler_options [
@@ -38,7 +94,7 @@ defmodule Code do
:warnings_as_errors
]
- @list_compiler_options [:no_warn_undefined]
+ @list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@available_compiler_options @boolean_compiler_options ++ @list_compiler_options
@@ -838,9 +894,10 @@ defmodule Code do
end
@doc """
- Gets the compilation options from the code server.
+ Gets all compilation options from the code server.
- Check `compiler_options/1` for more information.
+ To get invidual options, see `get_compiler_option/1`.
+ For a description of all options, see `put_compiler_option/2`.
## Examples
@@ -848,8 +905,7 @@ defmodule Code do
#=> %{debug_info: true, docs: true, ...}
"""
- # TODO: Deprecate me on Elixir v1.12
- @doc deprecated: "Use Code.compiler_option/1 instead"
+ @spec compiler_options :: map
def compiler_options do
for key <- @available_compiler_options, into: %{} do
{key, :elixir_config.get(key)}
@@ -857,26 +913,46 @@ defmodule Code do
end
@doc """
+ Stores all given compilation options.
+
+ To store invidual options, see `put_compiler_option/2`.
+ For a description of all options, see `put_compiler_option/2`.
+
+ ## Examples
+
+ Code.compiler_options()
+ #=> %{debug_info: true, docs: true, ...}
+
+ """
+ @spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
+ def compiler_options(opts) do
+ for {key, value} <- opts, into: %{} do
+ put_compiler_option(key, value)
+ {key, value}
+ end
+ end
+
+ @doc """
Returns the value of a given compiler option.
- Check `compiler_options/1` for more information.
+ For a description of all options, see `put_compiler_option/2`.
## Examples
- Code.compiler_option(:debug_info)
+ Code.get_compiler_option(:debug_info)
#=> true
"""
@doc since: "1.10.0"
- @spec compiler_option(atom) :: term
- def compiler_option(key) when key in @available_compiler_options do
+ @spec get_compiler_option(atom) :: term
+ def get_compiler_option(key) when key in @available_compiler_options do
:elixir_config.get(key)
end
@doc """
- Returns a list with the available compiler options.
+ Returns a list with all available compiler options.
- See `compiler_options/1` for more information.
+ For a description of all options, see `put_compiler_option/2`.
## Examples
@@ -890,30 +966,9 @@ defmodule Code do
end
@doc """
- Purge compiler modules.
-
- The compiler utilizes temporary modules to compile code. For example,
- `elixir_compiler_1`, `elixir_compiler_2`, etc. In case the compiled code
- stores references to anonymous functions or similar, the Elixir compiler
- may be unable to reclaim those modules, keeping an unnecessary amount of
- code in memory and eventually leading to modules such as `elixir_compiler_12345`.
-
- This function purges all modules currently kept by the compiler, allowing
- old compiler module names to be reused. If there are any processes running
- any code from such modules, they will be terminated too.
-
- It returns `{:ok, number_of_modules_purged}`.
- """
- @doc since: "1.7.0"
- @spec purge_compiler_modules() :: {:ok, non_neg_integer()}
- def purge_compiler_modules() do
- :elixir_code_server.call(:purge_compiler_modules)
- end
-
- @doc """
- Sets compilation options.
+ Stores a compilation option.
- These options are global since they are stored by Elixir's Code Server.
+ These options are global since they are stored by Elixir's code server.
Available options are:
@@ -934,40 +989,81 @@ defmodule Code do
* `:warnings_as_errors` - causes compilation to fail when warnings are
generated. Defaults to `false`.
- * `:no_warn_undefined` - list of modules and `{Mod, fun, arity}` tuples
- that will not emit warnings that the module or function does not exist
+ * `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
+ tuples that will not emit warnings that the module or function does not exist
at compilation time. This can be useful when doing dynamic compilation.
Defaults to `[]`.
- It returns the new map of compiler options.
+ * `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
+ compilation. See the module docs for more information. Defaults to `[]`.
+
+ * `:parser_options` (since v1.10.0) - a keyword list of options to be given
+ to the parser when compiling files. It accepts the same options as
+ `string_to_quoted/2` (except by the options that change the AST itself).
+ This can be used in combination with the tracer to retrieve localized
+ information about events happening during compilation. Defaults to `[]`.
+
+ It always returns `:ok`. Raises an error for invalid options.
## Examples
- Code.compiler_options(debug_info: true, ...)
- #=> %{debug_info: true, ...}
+ Code.put_compiler_option(:debug_info, true)
+ #=> :ok
"""
- @spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
- def compiler_options(opts) do
- for {key, value} <- opts, into: %{} do
- cond do
- key in @boolean_compiler_options ->
- if not is_boolean(value) do
- raise "compiler option #{inspect(key)} should be a boolean, got: #{inspect(value)}"
- end
+ @doc since: "1.10.0"
+ @spec put_compiler_option(atom, term) :: :ok
+ def put_compiler_option(key, value) when key in @boolean_compiler_options do
+ if not is_boolean(value) do
+ raise "compiler option #{inspect(key)} should be a boolean, got: #{inspect(value)}"
+ end
- key in @list_compiler_options ->
- if not is_list(value) do
- raise "compiler option #{inspect(key)} should be a list, got: #{inspect(value)}"
- end
+ :elixir_config.put(key, value)
+ :ok
+ end
- true ->
- raise "unknown compiler option: #{inspect(key)}"
- end
+ def put_compiler_option(key, value) when key in @list_compiler_options do
+ if not is_list(value) do
+ raise "compiler option #{inspect(key)} should be a list, got: #{inspect(value)}"
+ end
- :elixir_config.put(key, value)
- {key, value}
+ if key == :parser_options and not Keyword.keyword?(value) do
+ raise "compiler option #{inspect(key)} should be a keyword list, " <>
+ "got: #{inspect(value)}"
+ end
+
+ if key == :tracers and not Enum.all?(value, &is_atom/1) do
+ raise "compiler option #{inspect(key)} should be a list of modules, " <>
+ "got: #{inspect(value)}"
end
+
+ :elixir_config.put(key, value)
+ :ok
+ end
+
+ def put_compiler_option(key, _value) do
+ raise "unknown compiler option: #{inspect(key)}"
+ end
+
+ @doc """
+ Purge compiler modules.
+
+ The compiler utilizes temporary modules to compile code. For example,
+ `elixir_compiler_1`, `elixir_compiler_2`, etc. In case the compiled code
+ stores references to anonymous functions or similar, the Elixir compiler
+ may be unable to reclaim those modules, keeping an unnecessary amount of
+ code in memory and eventually leading to modules such as `elixir_compiler_12345`.
+
+ This function purges all modules currently kept by the compiler, allowing
+ old compiler module names to be reused. If there are any processes running
+ any code from such modules, they will be terminated too.
+
+ It returns `{:ok, number_of_modules_purged}`.
+ """
+ @doc since: "1.7.0"
+ @spec purge_compiler_modules() :: {:ok, non_neg_integer()}
+ def purge_compiler_modules() do
+ :elixir_code_server.call(:purge_compiler_modules)
end
@doc """
diff --git a/lib/elixir/lib/kernel/lexical_tracker.ex b/lib/elixir/lib/kernel/lexical_tracker.ex
index 4f53fb8ce..5de2c90f9 100644
--- a/lib/elixir/lib/kernel/lexical_tracker.ex
+++ b/lib/elixir/lib/kernel/lexical_tracker.ex
@@ -12,8 +12,8 @@ defmodule Kernel.LexicalTracker do
@doc """
Returns all remotes referenced in this lexical scope.
"""
- def remote_references(pid) do
- :gen_server.call(pid, :remote_references, @timeout)
+ def alias_references(pid) do
+ :gen_server.call(pid, :alias_references, @timeout)
end
# Internal API
@@ -119,7 +119,7 @@ defmodule Kernel.LexicalTracker do
{:reply, Enum.sort(directives), state}
end
- def handle_call(:remote_references, _from, state) do
+ def handle_call(:alias_references, _from, state) do
{compile, runtime} = partition(:maps.to_list(state.references), [], [])
{:reply, {compile, :maps.keys(state.structs), runtime}, state}
end
diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex
index 7143c2e0a..45ce9d228 100644
--- a/lib/elixir/lib/macro.ex
+++ b/lib/elixir/lib/macro.ex
@@ -1352,10 +1352,10 @@ defmodule Macro do
elem(do_expand_once(ast, env), 0)
end
- defp do_expand_once({:__aliases__, _, _} = original, env) do
- case :elixir_aliases.expand(original, env.aliases, env.macro_aliases, env.lexical_tracker) do
+ defp do_expand_once({:__aliases__, meta, _} = original, env) do
+ case :elixir_aliases.expand(original, env) do
receiver when is_atom(receiver) ->
- :elixir_lexical.record_remote(receiver, env.function, env.lexical_tracker)
+ :elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
aliases ->
@@ -1364,7 +1364,7 @@ defmodule Macro do
case :lists.all(&is_atom/1, aliases) do
true ->
receiver = :elixir_aliases.concat(aliases)
- :elixir_lexical.record_remote(receiver, env.function, env.lexical_tracker)
+ :elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
false ->
diff --git a/lib/elixir/lib/macro/env.ex b/lib/elixir/lib/macro/env.ex
index e5a2770ad..9d3b038c0 100644
--- a/lib/elixir/lib/macro/env.ex
+++ b/lib/elixir/lib/macro/env.ex
@@ -36,15 +36,16 @@ defmodule Macro.Env do
* `macros` - a list of macros imported from each module
* `macro_aliases` - a list of aliases defined inside the current macro
* `context_modules` - a list of modules defined in the current context
- * `lexical_tracker` - PID of the lexical tracker which is responsible for
- keeping user info
- The following fields pertain to variable handling and must not be accessed or
- relied on. To get a list of all variables, see `vars/1`:
+ The following fields are private to Elixir's macro expansion mechanism and
+ must not be accessed directly. See the functions in this module that exposes
+ the relevant information from the fields below whenever necessary:
* `current_vars`
* `prematch_vars`
* `contextual_vars`
+ * `lexical_tracker`
+ * `tracers`
The following fields are deprecated and must not be accessed or relied on:
@@ -74,6 +75,7 @@ defmodule Macro.Env do
@typep prematch_vars ::
%{optional(variable) => {var_version, var_type}} | :warn | :raise | :pin | :apply
@typep contextual_vars :: [atom]
+ @typep tracers :: [module]
@type t :: %{
__struct__: __MODULE__,
@@ -92,7 +94,8 @@ defmodule Macro.Env do
current_vars: current_vars,
prematch_vars: prematch_vars,
lexical_tracker: lexical_tracker,
- contextual_vars: contextual_vars
+ contextual_vars: contextual_vars,
+ tracers: tracers
}
# TODO: Remove :vars field on v2.0
@@ -114,7 +117,8 @@ defmodule Macro.Env do
current_vars: {%{}, %{}},
prematch_vars: :warn,
lexical_tracker: nil,
- contextual_vars: []
+ contextual_vars: [],
+ tracers: []
}
end
diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex
index 439708e53..f251e7b72 100644
--- a/lib/elixir/lib/module.ex
+++ b/lib/elixir/lib/module.ex
@@ -482,10 +482,10 @@ defmodule Module do
below:
* `@compile :debug_info` - includes `:debug_info` regardless of the
- corresponding setting in `Code.compiler_options/1`
+ corresponding setting in `Code.put_compiler_option/2`
* `@compile {:debug_info, false}` - disables `:debug_info` regardless
- of the corresponding setting in `Code.compiler_options/1`
+ of the corresponding setting in `Code.put_compiler_option/2`
* `@compile {:inline, some_fun: 2, other_fun: 3}` - inlines the given
name/arity pairs. Inlining is applied locally, calls from another
@@ -1537,7 +1537,7 @@ defmodule Module do
:ok
end
- defp check_behaviours(%{lexical_tracker: pid} = env, behaviours) do
+ defp check_behaviours(env, behaviours) do
Enum.reduce(behaviours, %{}, fn behaviour, acc ->
cond do
not is_atom(behaviour) ->
@@ -1562,7 +1562,7 @@ defmodule Module do
acc
true ->
- :elixir_lexical.record_remote(behaviour, nil, pid)
+ :elixir_env.trace({:require, [], behaviour, []}, env)
optional_callbacks = behaviour_info(behaviour, :optional_callbacks)
callbacks = behaviour_info(behaviour, :callbacks)
Enum.reduce(callbacks, acc, &add_callback(&1, behaviour, env, optional_callbacks, &2))
diff --git a/lib/elixir/lib/module/checker.ex b/lib/elixir/lib/module/checker.ex
index 39fa87535..48ad15523 100644
--- a/lib/elixir/lib/module/checker.ex
+++ b/lib/elixir/lib/module/checker.ex
@@ -67,7 +67,7 @@ defmodule Module.Checker do
end
defp warnings(map, cache) do
- no_warn_undefined = map.no_warn_undefined ++ Code.compiler_option(:no_warn_undefined)
+ no_warn_undefined = map.no_warn_undefined ++ Code.get_compiler_option(:no_warn_undefined)
state = %{
cache: cache,
diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl
index b6ca414da..566f8ded9 100644
--- a/lib/elixir/src/elixir.erl
+++ b/lib/elixir/src/elixir.erl
@@ -66,10 +66,12 @@ start(_Type, _Args) ->
%% Compiler options
{docs, true},
{ignore_module_conflict, false},
+ {parser_options, []},
{debug_info, true},
{warnings_as_errors, false},
{relative_paths, true},
- {no_warn_undefined, []}
+ {no_warn_undefined, []},
+ {tracers, []}
| URIConfig
],
@@ -168,11 +170,7 @@ start_cli() ->
%% EVAL HOOKS
env_for_eval(Opts) ->
- env_for_eval((elixir_env:new())#{
- requires := elixir_dispatch:default_requires(),
- functions := elixir_dispatch:default_functions(),
- macros := elixir_dispatch:default_macros()
- }, Opts).
+ env_for_eval(elixir_env:new(), Opts).
env_for_eval(Env, Opts) ->
Line = case lists:keyfind(line, 1, Opts) of
diff --git a/lib/elixir/src/elixir_aliases.erl b/lib/elixir/src/elixir_aliases.erl
index ec21f1d5c..8e561d929 100644
--- a/lib/elixir/src/elixir_aliases.erl
+++ b/lib/elixir/src/elixir_aliases.erl
@@ -1,6 +1,6 @@
-module(elixir_aliases).
-export([inspect/1, last/1, concat/1, safe_concat/1, format_error/1,
- ensure_loaded/3, expand/4, store/7]).
+ ensure_loaded/3, expand/2, store/5]).
-include("elixir.hrl").
inspect(Atom) when is_atom(Atom) ->
@@ -10,12 +10,11 @@ inspect(Atom) when is_atom(Atom) ->
end.
%% Store an alias in the given scope
-store(Meta, New, New, _TOpts, Aliases, MacroAliases, _Lexical) ->
+store(Meta, New, New, _TOpts, #{aliases := Aliases, macro_aliases := MacroAliases}) ->
{remove_alias(New, Aliases), remove_macro_alias(Meta, New, MacroAliases)};
-store(Meta, New, Old, TOpts, Aliases, MacroAliases, Lexical) ->
- record_warn(Meta, New, TOpts, Lexical),
- {store_alias(New, Old, Aliases),
- store_macro_alias(Meta, New, Old, MacroAliases)}.
+store(Meta, New, Old, TOpts, #{aliases := Aliases, macro_aliases := MacroAliases} = E) ->
+ elixir_env:trace({alias, Meta, Old, New, TOpts}, E),
+ {store_alias(New, Old, Aliases), store_macro_alias(Meta, New, Old, MacroAliases)}.
store_alias(New, Old, Aliases) ->
lists:keystore(New, 1, Aliases, {New, Old}).
@@ -39,32 +38,23 @@ remove_macro_alias(Meta, Atom, Aliases) ->
Aliases
end.
-record_warn(Meta, Ref, Opts, Lexical) ->
- Warn =
- case lists:keyfind(warn, 1, Opts) of
- {warn, false} -> false;
- {warn, true} -> true;
- false -> not lists:keymember(context, 1, Meta)
- end,
- elixir_lexical:record_alias(Ref, ?line(Meta), Warn, Lexical).
-
%% Expand an alias. It returns an atom (meaning that there
%% was an expansion) or a list of atoms.
-expand({'__aliases__', _Meta, ['Elixir' | _] = List}, _Aliases, _MacroAliases, _LexicalTracker) ->
+expand({'__aliases__', _Meta, ['Elixir' | _] = List}, _E) ->
concat(List);
-expand({'__aliases__', Meta, _} = Alias, Aliases, MacroAliases, LexicalTracker) ->
+expand({'__aliases__', Meta, _} = Alias, #{aliases := Aliases, macro_aliases := MacroAliases} = E) ->
case lists:keyfind(alias, 1, Meta) of
{alias, false} ->
- expand(Alias, MacroAliases, LexicalTracker);
+ expand(Alias, MacroAliases, E);
{alias, Atom} when is_atom(Atom) ->
Atom;
false ->
- expand(Alias, Aliases, LexicalTracker)
+ expand(Alias, Aliases, E)
end.
-expand({'__aliases__', Meta, [H | T]}, Aliases, LexicalTracker) when is_atom(H) ->
+expand({'__aliases__', Meta, [H | T]}, Aliases, E) when is_atom(H) ->
Lookup = list_to_atom("Elixir." ++ atom_to_list(H)),
Counter = case lists:keyfind(counter, 1, Meta) of
{counter, C} -> C;
@@ -73,14 +63,14 @@ expand({'__aliases__', Meta, [H | T]}, Aliases, LexicalTracker) when is_atom(H)
case lookup(Lookup, Aliases, Counter) of
Lookup -> [H | T];
Atom ->
- elixir_lexical:record_alias(Lookup, LexicalTracker),
+ elixir_env:trace({alias_expansion, Meta, Lookup, Atom}, E),
case T of
[] -> Atom;
_ -> concat([Atom | T])
end
end;
-expand({'__aliases__', _Meta, List}, _Aliases, _LexicalTracker) ->
+expand({'__aliases__', _Meta, List}, _Aliases, _E) ->
List.
%% Ensure a module is loaded before its usage.
diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl
index bde108883..9918f6e60 100644
--- a/lib/elixir/src/elixir_compiler.erl
+++ b/lib/elixir/src/elixir_compiler.erl
@@ -5,7 +5,7 @@
-include("elixir.hrl").
string(Contents, File, Callback) ->
- Forms = elixir:'string_to_quoted!'(Contents, 1, File, []),
+ Forms = elixir:'string_to_quoted!'(Contents, 1, File, elixir_config:get(parser_options)),
quoted(Forms, File, Callback).
quoted(Forms, File, Callback) ->
@@ -13,11 +13,13 @@ quoted(Forms, File, Callback) ->
try
put(elixir_module_binaries, []),
- elixir_lexical:run(File, fun(Pid) ->
- Env = elixir:env_for_eval([{line, 1}, {file, File}]),
- eval_forms(Forms, [], Env#{lexical_tracker := Pid}),
+ Env = (elixir_env:new())#{line := 1, file := File, tracers := elixir_config:get(tracers)},
+
+ elixir_lexical:run(Env, fun(#{lexical_tracker := Pid} = LexicalEnv) ->
+ eval_forms(Forms, [], LexicalEnv),
Callback(File, Pid)
end),
+
lists:reverse(get(elixir_module_binaries))
after
put(elixir_module_binaries, Previous)
@@ -119,6 +121,8 @@ bootstrap() ->
elixir_config:put(docs, false),
elixir_config:put(relative_paths, false),
elixir_config:put(ignore_module_conflict, true),
+ elixir_config:put(tracers, []),
+ elixir_config:put(parser_options, []),
[bootstrap_file(File) || File <- bootstrap_main()].
bootstrap_file(File) ->
diff --git a/lib/elixir/src/elixir_def.erl b/lib/elixir/src/elixir_def.erl
index d23b2f6cb..81429f563 100644
--- a/lib/elixir/src/elixir_def.erl
+++ b/lib/elixir/src/elixir_def.erl
@@ -210,15 +210,7 @@ run_with_location_change(nil, E, Callback) ->
run_with_location_change(File, #{file := File} = E, Callback) ->
Callback(E);
run_with_location_change(File, E, Callback) ->
- EL = E#{file := File},
- Tracker = ?key(E, lexical_tracker),
-
- try
- elixir_lexical:set_file(File, Tracker),
- Callback(EL)
- after
- elixir_lexical:reset_file(Tracker)
- end.
+ elixir_lexical:with_file(File, E, Callback).
def_to_clauses(_Kind, Meta, Args, [], nil, E) ->
check_args_for_function_head(Meta, Args, E),
diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl
index 96d8411e7..39c81a811 100644
--- a/lib/elixir/src/elixir_dispatch.erl
+++ b/lib/elixir/src/elixir_dispatch.erl
@@ -26,10 +26,10 @@ find_import(Meta, Name, Arity, E) ->
case find_dispatch(Meta, Tuple, [], E) of
{function, Receiver} ->
- elixir_lexical:record_import(Receiver, Name, Arity, ?key(E, lexical_tracker)),
+ elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E),
Receiver;
{macro, Receiver} ->
- elixir_lexical:record_import(Receiver, Name, Arity, ?key(E, lexical_tracker)),
+ elixir_env:trace({imported_macro, Meta, Receiver, Name, Arity}, E),
Receiver;
_ ->
false
@@ -41,7 +41,7 @@ import_function(Meta, Name, Arity, E) ->
Tuple = {Name, Arity},
case find_dispatch(Meta, Tuple, [], E) of
{function, Receiver} ->
- elixir_lexical:record_import(Receiver, Name, Arity, ?key(E, lexical_tracker)),
+ elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E),
elixir_locals:record_import(Tuple, Receiver, ?key(E, module), ?key(E, function)),
remote_function(Meta, Receiver, Name, Arity, E);
{macro, _Receiver} ->
@@ -50,10 +50,20 @@ import_function(Meta, Name, Arity, E) ->
require_function(Meta, Receiver, Name, Arity, E);
false ->
case elixir_import:special_form(Name, Arity) of
- true -> false;
+ true ->
+ false;
false ->
- elixir_locals:record_local(Tuple, ?key(E, module), ?key(E, function), Meta, false),
- {local, Name, Arity}
+ Function = ?key(E, function),
+
+ case (Function /= nil) andalso (Function /= Tuple) andalso
+ elixir_def:local_for(?key(E, module), Name, Arity, [defmacro, defmacrop]) of
+ false ->
+ elixir_env:trace({local_function, Meta, Name, Arity}, E),
+ elixir_locals:record_local(Tuple, ?key(E, module), ?key(E, function), Meta, false),
+ {local, Name, Arity};
+ _ ->
+ false
+ end
end
end.
@@ -62,7 +72,7 @@ require_function(Meta, Receiver, Name, Arity, E) ->
case is_element({Name, Arity}, get_macros(Receiver, Required)) of
true -> false;
false ->
- elixir_lexical:record_remote(Receiver, ?key(E, function), ?key(E, lexical_tracker)),
+ elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E),
remote_function(Meta, Receiver, Name, Arity, E)
end.
@@ -131,6 +141,7 @@ expand_import(Meta, {Name, Arity} = Tuple, Args, E, Extra, External) ->
%% Dispatch to the local.
_ ->
+ elixir_env:trace({local_macro, Meta, Name, Arity}, E),
elixir_locals:record_local(Tuple, Module, Function, Meta, true),
{ok, Module, expand_macro_fun(Meta, Local, Module, Name, Args, E)}
end
@@ -139,12 +150,12 @@ expand_import(Meta, {Name, Arity} = Tuple, Args, E, Extra, External) ->
do_expand_import(Meta, {Name, Arity} = Tuple, Args, Module, E, Result) ->
case Result of
{function, Receiver} ->
- elixir_lexical:record_import(Receiver, Name, Arity, ?key(E, lexical_tracker)),
+ elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E),
elixir_locals:record_import(Tuple, Receiver, Module, ?key(E, function)),
{ok, Receiver, Name, Args};
{macro, Receiver} ->
check_deprecated(Meta, Receiver, Name, Arity, E),
- elixir_lexical:record_import(Receiver, Name, Arity, ?key(E, lexical_tracker)),
+ elixir_env:trace({imported_macro, Meta, Receiver, Name, Arity}, E),
elixir_locals:record_import(Tuple, Receiver, Module, ?key(E, function)),
{ok, Receiver, expand_macro_named(Meta, Receiver, Name, Arity, Args, E)};
{import, Receiver} ->
@@ -167,7 +178,7 @@ expand_require(Meta, Receiver, {Name, Arity} = Tuple, Args, E) ->
case is_element(Tuple, get_macros(Receiver, Required)) of
true when Required ->
- elixir_lexical:record_remote(Receiver, nil, ?key(E, lexical_tracker)),
+ elixir_env:trace({remote_macro, Meta, Receiver, Name, Arity}, E),
{ok, Receiver, expand_macro_named(Meta, Receiver, Name, Arity, Args, E)};
true ->
Info = {unrequired_module, {Receiver, Name, length(Args)}},
diff --git a/lib/elixir/src/elixir_env.erl b/lib/elixir/src/elixir_env.erl
index d5649510f..aeb0d4c6a 100644
--- a/lib/elixir/src/elixir_env.erl
+++ b/lib/elixir/src/elixir_env.erl
@@ -4,27 +4,34 @@
new/0, linify/1, with_vars/2, reset_vars/1,
env_to_scope/1, env_to_scope_with_vars/2,
check_unused_vars/1, merge_and_check_unused_vars/2,
- mergea/2, mergev/2, format_error/1
+ trace/2, mergea/2, mergev/2, format_error/1
]).
new() ->
- #{'__struct__' => 'Elixir.Macro.Env',
- module => nil, %% the current module
- file => <<"nofile">>, %% the current filename
- line => 1, %% the current line
- function => nil, %% the current function
- context => nil, %% can be match, guard or nil
- requires => [], %% a set with modules required
- aliases => [], %% a list of aliases by new -> old names
- functions => [], %% a list with functions imported from module
- macros => [], %% a list with macros imported from module
- macro_aliases => [], %% keep aliases defined inside a macro
- context_modules => [], %% modules defined in the current context
- vars => [], %% a set of defined variables
- current_vars => {#{}, #{}}, %% a tuple with maps of current and unused variables
- prematch_vars => warn, %% behaviour outside and inside matches
- lexical_tracker => nil, %% holds the lexical tracker PID
- contextual_vars => []}. %% holds available contextual variables
+ #{
+ '__struct__' => 'Elixir.Macro.Env',
+ module => nil, %% the current module
+ file => <<"nofile">>, %% the current filename
+ line => 1, %% the current line
+ function => nil, %% the current function
+ context => nil, %% can be match, guard or nil
+ aliases => [], %% a list of aliases by new -> old names
+ requires => elixir_dispatch:default_requires(), %% a set with modules required
+ functions => elixir_dispatch:default_functions(), %% a list with functions imported from module
+ macros => elixir_dispatch:default_macros(), %% a list with macros imported from module
+ macro_aliases => [], %% keep aliases defined inside a macro
+ context_modules => [], %% modules defined in the current context
+ vars => [], %% a set of defined variables
+ current_vars => {#{}, #{}}, %% a tuple with maps of current and unused variables
+ prematch_vars => warn, %% behaviour outside and inside matches
+ lexical_tracker => nil, %% holds the lexical tracker PID
+ contextual_vars => [], %% holds available contextual variables
+ tracers => [] %% holds the available compilation tracers
+ }.
+
+trace(Event, #{tracers := Tracers} = E) ->
+ [ok = Tracer:trace(Event, E) || Tracer <- Tracers],
+ ok.
linify({Line, Env}) ->
Env#{line := Line};
diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl
index 16cabd6f6..8e002e973 100644
--- a/lib/elixir/src/elixir_expand.erl
+++ b/lib/elixir/src/elixir_expand.erl
@@ -528,7 +528,7 @@ expand_fn_capture(Meta, Arg, E) ->
case elixir_fn:capture(Meta, Arg, E) of
{{remote, Remote, Fun, Arity}, EE} ->
is_atom(Remote) andalso
- elixir_lexical:record_remote(Remote, ?key(E, function), ?key(E, lexical_tracker)),
+ elixir_env:trace({remote_function, Meta, Remote, Fun, Arity}, E),
AttachedMeta = attach_context_module(Remote, Meta, E),
{{'&', AttachedMeta, [{'/', [], [{{'.', [], [Remote, Fun]}, [], []}, Arity]}]}, EE};
{{local, Fun, Arity}, #{function := nil}} ->
@@ -788,7 +788,8 @@ expand_local(Meta, Name, Args, #{context := Context} = E) when Context == match;
form_error(Meta, E, ?MODULE, {invalid_local_invocation, Context, {Name, Meta, Args}});
expand_local(Meta, Name, Args, #{module := Module, function := Function} = E) ->
assert_no_clauses(Name, Meta, Args, E),
-
+ Arity = length(Args),
+ elixir_env:trace({local_function, Meta, Name, Arity}, E),
elixir_locals:record_local({Name, length(Args)}, Module, Function, Meta, false),
{EArgs, EA} = expand_args(Args, E),
{{Name, Meta, EArgs}, EA}.
@@ -797,10 +798,11 @@ expand_local(Meta, Name, Args, #{module := Module, function := Function} = E) ->
expand_remote(Receiver, DotMeta, Right, Meta, Args, #{context := Context} = E, EL) when is_atom(Receiver) or is_tuple(Receiver) ->
assert_no_clauses(Right, Meta, Args, E),
+ AttachedDotMeta = attach_context_module(Receiver, DotMeta, E),
is_atom(Receiver) andalso
- elixir_lexical:record_remote(Receiver, ?key(E, function), ?key(E, lexical_tracker)),
- AttachedDotMeta = attach_context_module(Receiver, DotMeta, E),
+ elixir_env:trace({remote_function, DotMeta, Receiver, Right, length(Args)}, E),
+
{EArgs, EA} = expand_args(Args, E),
case rewrite(Context, Receiver, AttachedDotMeta, Right, Meta, EArgs) of
{ok, Rewritten} ->
@@ -905,9 +907,7 @@ no_alias_expansion(Other) ->
Other.
expand_require(Meta, Ref, Opts, E) ->
- %% We always record requires when they are defined
- %% as they expect the reference at compile time.
- elixir_lexical:record_remote(Ref, nil, ?key(E, lexical_tracker)),
+ elixir_env:trace({require, Meta, Ref, Opts}, E),
RE = E#{requires := ordsets:add_element(Ref, ?key(E, requires))},
expand_alias(Meta, false, Ref, Opts, RE).
@@ -923,9 +923,7 @@ expand_alias(Meta, IncludeByDefault, Ref, Opts, #{context_modules := Context} =
false -> Context
end,
- {Aliases, MacroAliases} = elixir_aliases:store(Meta, New, Ref, Opts, ?key(E, aliases),
- ?key(E, macro_aliases), ?key(E, lexical_tracker)),
-
+ {Aliases, MacroAliases} = elixir_aliases:store(Meta, New, Ref, Opts, E),
E#{aliases := Aliases, macro_aliases := MacroAliases, context_modules := NewContext}.
expand_as({as, nil}, _Meta, _IncludeByDefault, Ref, _E) ->
@@ -957,19 +955,18 @@ expand_without_aliases_report(Other, E) ->
expand(Other, E).
expand_aliases({'__aliases__', Meta, _} = Alias, E, Report) ->
- case elixir_aliases:expand(Alias, ?key(E, aliases), ?key(E, macro_aliases), ?key(E, lexical_tracker)) of
+ case elixir_aliases:expand(Alias, E) of
Receiver when is_atom(Receiver) ->
- Report andalso
- elixir_lexical:record_remote(Receiver, ?key(E, function), ?key(E, lexical_tracker)),
+ Report andalso elixir_env:trace({alias_reference, Meta, Receiver}, E),
{Receiver, E};
+
Aliases ->
{EAliases, EA} = expand_args(Aliases, E),
case lists:all(fun is_atom/1, EAliases) of
true ->
Receiver = elixir_aliases:concat(EAliases),
- Report andalso
- elixir_lexical:record_remote(Receiver, ?key(E, function), ?key(E, lexical_tracker)),
+ Report andalso elixir_env:trace({alias_reference, Meta, Receiver}, E),
{Receiver, EA};
false ->
form_error(Meta, E, ?MODULE, {invalid_alias, Alias})
diff --git a/lib/elixir/src/elixir_import.erl b/lib/elixir/src/elixir_import.erl
index d80117987..afd64ee03 100644
--- a/lib/elixir/src/elixir_import.erl
+++ b/lib/elixir/src/elixir_import.erl
@@ -24,7 +24,7 @@ import(Meta, Ref, Opts, E) ->
{Funs, Macs, Added1 or Added2}
end,
- record_warn(Meta, Ref, Opts, Added, E),
+ elixir_env:trace({import, [{imported, Added} | Meta], Ref, Opts}, E),
{Functions, Macros}.
import_functions(Meta, Ref, Opts, E) ->
@@ -44,22 +44,6 @@ import_macros(Force, Meta, Ref, Opts, E) ->
end
end).
-record_warn(Meta, Ref, Opts, Added, E) ->
- Warn =
- case keyfind(warn, Opts) of
- {warn, false} -> false;
- {warn, true} -> true;
- false -> not lists:keymember(context, 1, Meta)
- end,
-
- Only =
- case keyfind(only, Opts) of
- {only, List} when is_list(List) -> List;
- _ -> []
- end,
-
- elixir_lexical:record_import(Ref, Only, ?line(Meta), Added and Warn, ?key(E, lexical_tracker)).
-
%% Calculates the imports based on only and except
calculate(Meta, Key, Opts, Old, File, Existing) ->
diff --git a/lib/elixir/src/elixir_lexical.erl b/lib/elixir/src/elixir_lexical.erl
index 319dae8b8..628a6537a 100644
--- a/lib/elixir/src/elixir_lexical.erl
+++ b/lib/elixir/src/elixir_lexical.erl
@@ -1,84 +1,111 @@
%% Module responsible for tracking lexical information.
-module(elixir_lexical).
--export([run/2, set_file/2, reset_file/1,
- record_alias/4, record_alias/2,
- record_import/4, record_import/5,
- record_remote/3, record_struct/2,
- format_error/1
-]).
+-export([run/2, with_file/3, trace/2, format_error/1]).
-include("elixir.hrl").
-define(tracker, 'Elixir.Kernel.LexicalTracker').
-run(File, Callback) ->
+run(#{tracers := Tracers} = E, Callback) ->
case elixir_config:get(bootstrap) of
false ->
{ok, Pid} = ?tracker:start_link(),
- try Callback(Pid) of
+
+ try Callback(E#{lexical_tracker := Pid, tracers := [?MODULE | Tracers]}) of
Res ->
- warn_unused_aliases(File, Pid),
- warn_unused_imports(File, Pid),
+ warn_unused_aliases(Pid, E),
+ warn_unused_imports(Pid, E),
Res
after
unlink(Pid),
?tracker:stop(Pid)
end;
+
true ->
- Callback(nil)
+ Callback(E)
end.
-%% RECORD
-
-record_alias(Module, Line, Warn, Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:add_alias(Pid, Module, Line, Warn), ok end).
-
-record_import(Module, FAs, Line, Warn, Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:add_import(Pid, Module, FAs, Line, Warn), ok end).
-
-record_alias(Module, Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:alias_dispatch(Pid, Module), ok end).
-
-record_import(Module, Function, Arity, Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:import_dispatch(Pid, Module, {Function, Arity}), ok end).
+trace({import, Meta, Module, Opts}, #{lexical_tracker := Pid}) ->
+ {imported, Imported} = lists:keyfind(imported, 1, Meta),
+
+ Only =
+ case lists:keyfind(only, 1, Opts) of
+ {only, List} when is_list(List) -> List;
+ _ -> []
+ end,
+
+ ?tracker:add_import(Pid, Module, Only, ?line(Meta), Imported and should_warn(Meta, Opts)),
+ ok;
+trace({alias, Meta, _Old, New, Opts}, #{lexical_tracker := Pid}) ->
+ ?tracker:add_alias(Pid, New, ?line(Meta), should_warn(Meta, Opts)),
+ ok;
+trace({alias_expansion, _Meta, Lookup, _Result}, #{lexical_tracker := Pid}) ->
+ ?tracker:alias_dispatch(Pid, Lookup),
+ ok;
+trace({require, _Meta, Module, _Opts}, #{lexical_tracker := Pid}) ->
+ %% We always record requires when they are defined
+ %% as they expect the reference at compile time.
+ ?tracker:remote_dispatch(Pid, Module, compile),
+ ok;
+trace({struct_expansion, _Meta, Module}, #{lexical_tracker := Pid}) ->
+ ?tracker:remote_struct(Pid, Module),
+ ok;
+trace({alias_reference, _Meta, Module}, #{lexical_tracker := Pid} = E) ->
+ ?tracker:remote_dispatch(Pid, Module, mode(E)),
+ ok;
+trace({remote_function, _Meta, Module, _Function, _Arity}, #{lexical_tracker := Pid} = E) ->
+ ?tracker:remote_dispatch(Pid, Module, mode(E)),
+ ok;
+trace({remote_macro, _Meta, Module, _Function, _Arity}, #{lexical_tracker := Pid}) ->
+ ?tracker:remote_dispatch(Pid, Module, compile),
+ ok;
+trace({imported_function, _Meta, Module, Function, Arity}, #{lexical_tracker := Pid}) ->
+ ?tracker:import_dispatch(Pid, Module, {Function, Arity}),
+ ok;
+trace({imported_macro, _Meta, Module, Function, Arity}, #{lexical_tracker := Pid}) ->
+ ?tracker:import_dispatch(Pid, Module, {Function, Arity}),
+ ok;
+trace(_, _) ->
+ ok.
-record_remote(Module, EnvFunction, Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:remote_dispatch(Pid, Module, mode(EnvFunction)), ok end).
+mode(#{function := nil}) -> compile;
+mode(#{}) -> runtime.
-record_struct(Module, Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:remote_struct(Pid, Module), ok end).
+should_warn(Meta, Opts) ->
+ case lists:keyfind(warn, 1, Opts) of
+ {warn, false} -> false;
+ {warn, true} -> true;
+ false -> not lists:keymember(context, 1, Meta)
+ end.
%% EXTERNAL SOURCES
-set_file(File, Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:set_file(Pid, File), ok end).
-
-reset_file(Ref) ->
- if_tracker(Ref, fun(Pid) -> ?tracker:reset_file(Pid), ok end).
-
-%% HELPERS
-
-mode(nil) -> compile;
-mode({_, _}) -> runtime.
-
-if_tracker(nil, _Callback) -> ok;
-if_tracker(Pid, Callback) when is_pid(Pid) -> Callback(Pid).
+with_file(File, #{lexical_tracker := nil} = E, Callback) ->
+ Callback(E#{file := File});
+with_file(File, #{lexical_tracker := Pid} = E, Callback) ->
+ try
+ ?tracker:set_file(Pid, File),
+ Callback(E#{file := File})
+ after
+ ?tracker:reset_file(Pid)
+ end.
%% ERROR HANDLING
-warn_unused_imports(File, Pid) ->
+warn_unused_imports(Pid, E) ->
{ModuleImports, MFAImports} =
lists:partition(fun({M, _}) -> is_atom(M) end, ?tracker:collect_unused_imports(Pid)),
+
Modules = [M || {M, _L} <- ModuleImports],
MFAImportsFiltered = [T || {{M, _, _}, _} = T <- MFAImports, not lists:member(M, Modules)],
[begin
- elixir_errors:form_warn([{line, L}], File, ?MODULE, {unused_import, M})
+ elixir_errors:form_warn([{line, L}], ?key(E, file), ?MODULE, {unused_import, M})
end || {M, L} <- ModuleImports ++ MFAImportsFiltered],
ok.
-warn_unused_aliases(File, Pid) ->
+warn_unused_aliases(Pid, E) ->
[begin
- elixir_errors:form_warn([{line, L}], File, ?MODULE, {unused_alias, M})
+ elixir_errors:form_warn([{line, L}], ?key(E, file), ?MODULE, {unused_alias, M})
end || {M, L} <- ?tracker:collect_unused_aliases(Pid)],
ok.
diff --git a/lib/elixir/src/elixir_map.erl b/lib/elixir/src/elixir_map.erl
index e3d087ee0..bcc23c265 100644
--- a/lib/elixir/src/elixir_map.erl
+++ b/lib/elixir/src/elixir_map.erl
@@ -20,9 +20,7 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, #{context := Context} = E)
case validate_struct(ELeft, Context) of
true when is_atom(ELeft) ->
- %% We always record structs when they are expanded
- %% as they expect the reference at compile time.
- elixir_lexical:record_struct(ELeft, ?key(E, lexical_tracker)),
+ elixir_env:trace({struct_expansion, Meta, ELeft}, E),
case extract_struct_assocs(Meta, ERight, E) of
{expand, MapMeta, Assocs} when Context /= match -> %% Expand
diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl
index e67a5ec1b..512148278 100644
--- a/lib/elixir/src/elixir_module.erl
+++ b/lib/elixir/src/elixir_module.erl
@@ -60,18 +60,19 @@ compile(Module, Block, Vars, #{line := Line, current_vars := {Current, _}} = Env
%% In case we are generating a module from inside a function,
%% we get rid of the lexical tracker information as, at this
%% point, the lexical tracker process is long gone.
- LexEnv = case ?key(Env, function) of
- nil -> Env#{module := Module, current_vars := {Current, #{}}};
- _ -> Env#{lexical_tracker := nil, function := nil, module := Module, current_vars := {Current, #{}}}
- end,
+ MaybeLexEnv =
+ case ?key(Env, function) of
+ nil -> Env#{module := Module, current_vars := {Current, #{}}};
+ _ -> Env#{lexical_tracker := nil, function := nil, module := Module, current_vars := {Current, #{}}}
+ end,
- case ?key(LexEnv, lexical_tracker) of
- nil ->
- elixir_lexical:run(?key(LexEnv, file), fun(Pid) ->
- compile(Line, Module, Block, Vars, LexEnv#{lexical_tracker := Pid})
+ case MaybeLexEnv of
+ #{lexical_tracker := nil} ->
+ elixir_lexical:run(MaybeLexEnv, fun(LexEnv) ->
+ compile(Line, Module, Block, Vars, LexEnv)
end);
_ ->
- compile(Line, Module, Block, Vars, LexEnv)
+ compile(Line, Module, Block, Vars, MaybeLexEnv)
end;
compile(Module, _Block, _Vars, #{line := Line, file := File}) ->
elixir_errors:form_error([{line, Line}], File, ?MODULE, {invalid_module, Module}).
diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl
index 951ca5558..0fdf8a827 100644
--- a/lib/elixir/src/elixir_quote.erl
+++ b/lib/elixir/src/elixir_quote.erl
@@ -215,7 +215,7 @@ do_quote({unquote, _Meta, [Expr]}, #elixir_quote{unquote=true}, _) ->
do_quote({'__aliases__', Meta, [H | T]} = Alias, #elixir_quote{aliases_hygiene=true} = Q, E) when is_atom(H) and (H /= 'Elixir') ->
Annotation =
- case elixir_aliases:expand(Alias, ?key(E, aliases), ?key(E, macro_aliases), ?key(E, lexical_tracker)) of
+ case elixir_aliases:expand(Alias, E) of
Atom when is_atom(Atom) -> Atom;
Aliases when is_list(Aliases) -> false
end,
diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs
index 80872cb48..1bb3ab2f2 100644
--- a/lib/elixir/test/elixir/code_test.exs
+++ b/lib/elixir/test/elixir/code_test.exs
@@ -8,7 +8,7 @@ defmodule CodeTest do
def genmodule(name) do
defmodule name do
- Kernel.LexicalTracker.remote_references(__ENV__.lexical_tracker)
+ Kernel.LexicalTracker.alias_references(__ENV__.lexical_tracker)
end
end
@@ -406,17 +406,17 @@ defmodule CodeTest do
refute Code.ensure_compiled?(Code.NoFile)
end
- test "compiler_options/1 validates options" do
+ test "put_compiler_option/2 validates options" do
message = "unknown compiler option: :not_a_valid_option"
assert_raise RuntimeError, message, fn ->
- Code.compiler_options(not_a_valid_option: :foo)
+ Code.put_compiler_option(:not_a_valid_option, :foo)
end
message = "compiler option :debug_info should be a boolean, got: :not_a_boolean"
assert_raise RuntimeError, message, fn ->
- Code.compiler_options(debug_info: :not_a_boolean)
+ Code.put_compiler_option(:debug_info, :not_a_boolean)
end
end
end
diff --git a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs
index 15c574da1..311a34b58 100644
--- a/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs
+++ b/lib/elixir/test/elixir/kernel/lexical_tracker_test.exs
@@ -12,45 +12,45 @@ defmodule Kernel.LexicalTrackerTest do
test "can add remote dispatch", config do
D.remote_dispatch(config[:pid], String, :runtime)
- assert D.remote_references(config[:pid]) == {[], [], [String]}
+ assert D.alias_references(config[:pid]) == {[], [], [String]}
D.remote_dispatch(config[:pid], String, :compile)
- assert D.remote_references(config[:pid]) == {[String], [], []}
+ assert D.alias_references(config[:pid]) == {[String], [], []}
D.remote_dispatch(config[:pid], String, :runtime)
- assert D.remote_references(config[:pid]) == {[String], [], []}
+ assert D.alias_references(config[:pid]) == {[String], [], []}
end
test "can add remote structs", config do
D.remote_struct(config[:pid], URI)
- assert D.remote_references(config[:pid]) == {[], [URI], []}
+ assert D.alias_references(config[:pid]) == {[], [URI], []}
D.remote_dispatch(config[:pid], URI, :runtime)
- assert D.remote_references(config[:pid]) == {[], [URI], [URI]}
+ assert D.alias_references(config[:pid]) == {[], [URI], [URI]}
D.remote_dispatch(config[:pid], URI, :compile)
- assert D.remote_references(config[:pid]) == {[URI], [URI], []}
+ assert D.alias_references(config[:pid]) == {[URI], [URI], []}
end
test "can add module imports", config do
D.add_import(config[:pid], String, [], 1, true)
D.import_dispatch(config[:pid], String, {:upcase, 1})
- assert D.remote_references(config[:pid]) == {[String], [], []}
+ assert D.alias_references(config[:pid]) == {[String], [], []}
D.import_dispatch(config[:pid], String, {:upcase, 1})
- assert D.remote_references(config[:pid]) == {[String], [], []}
+ assert D.alias_references(config[:pid]) == {[String], [], []}
end
test "can add module with {function, arity} imports", config do
D.add_import(config[:pid], String, [upcase: 1], 1, true)
D.import_dispatch(config[:pid], String, {:upcase, 1})
- assert D.remote_references(config[:pid]) == {[String], [], []}
+ assert D.alias_references(config[:pid]) == {[String], [], []}
end
test "can add aliases", config do
D.add_alias(config[:pid], String, 1, true)
D.alias_dispatch(config[:pid], String)
- assert D.remote_references(config[:pid]) == {[], [], []}
+ assert D.alias_references(config[:pid]) == {[], [], []}
end
test "unused module imports", config do
@@ -116,7 +116,7 @@ defmodule Kernel.LexicalTrackerTest do
@macrocallback foo2(Foo.Bar.t) :: Foo.Bar.t
@spec foo(bar3) :: Foo.Bar.t
def foo(_), do: :bar
- Kernel.LexicalTracker.remote_references(__ENV__.lexical_tracker)
+ Kernel.LexicalTracker.alias_references(__ENV__.lexical_tracker)
end |> elem(3)
""")
diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs
index da4ac670c..7ff0b5e61 100644
--- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs
+++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs
@@ -265,7 +265,7 @@ defmodule Kernel.ParallelCompilerTest do
end
test "supports warnings as errors" do
- warnings_as_errors = Code.compiler_option(:warnings_as_errors)
+ warnings_as_errors = Code.get_compiler_option(:warnings_as_errors)
[fixture] =
write_tmp(
@@ -408,7 +408,7 @@ defmodule Kernel.ParallelCompilerTest do
end
test "supports warnings as errors" do
- warnings_as_errors = Code.compiler_option(:warnings_as_errors)
+ warnings_as_errors = Code.get_compiler_option(:warnings_as_errors)
[fixture] =
write_tmp(
diff --git a/lib/elixir/test/elixir/kernel/tracers_test.exs b/lib/elixir/test/elixir/kernel/tracers_test.exs
new file mode 100644
index 000000000..4495f63f8
--- /dev/null
+++ b/lib/elixir/test/elixir/kernel/tracers_test.exs
@@ -0,0 +1,160 @@
+Code.require_file("../test_helper.exs", __DIR__)
+
+defmodule Kernel.TracersTest do
+ use ExUnit.Case
+
+ import Code, only: [compile_string: 1]
+
+ def trace(event, %Macro.Env{}) do
+ send(self(), event)
+ :ok
+ end
+
+ setup_all do
+ Code.put_compiler_option(:tracers, [__MODULE__])
+ Code.put_compiler_option(:parser_options, columns: true)
+
+ on_exit(fn ->
+ Code.put_compiler_option(:tracers, [])
+ Code.put_compiler_option(:parser_options, [])
+ end)
+ end
+
+ test "traces alias references" do
+ compile_string("""
+ Foo
+ """)
+
+ assert_receive {:alias_reference, meta, Foo}
+ assert meta[:line] == 1
+ assert meta[:column] == 1
+ end
+
+ test "traces aliases" do
+ compile_string("""
+ alias Hello.World
+ World
+
+ alias Foo, as: Bar, warn: true
+ Bar
+ """)
+
+ assert_receive {:alias, meta, Hello.World, World, []}
+ assert meta[:line] == 1
+ assert meta[:column] == 1
+ assert_receive {:alias_expansion, meta, World, Hello.World}
+ assert meta[:line] == 2
+ assert meta[:column] == 1
+
+ assert_receive {:alias, meta, Foo, Bar, [as: Bar, warn: true]}
+ assert meta[:line] == 4
+ assert meta[:column] == 1
+ assert_receive {:alias_expansion, meta, Bar, Foo}
+ assert meta[:line] == 5
+ assert meta[:column] == 1
+ end
+
+ test "traces imports" do
+ compile_string("""
+ import Integer, only: [is_odd: 1, parse: 1]
+ true = is_odd(1)
+ {1, ""} = parse("1")
+ """)
+
+ assert_receive {:import, meta, Integer, only: [is_odd: 1, parse: 1]}
+ assert meta[:line] == 1
+ assert meta[:column] == 1
+
+ assert_receive {:imported_macro, meta, Integer, :is_odd, 1}
+ assert meta[:line] == 2
+ assert meta[:column] == 8
+
+ assert_receive {:imported_function, meta, Integer, :parse, 1}
+ assert meta[:line] == 3
+ assert meta[:column] == 11
+ end
+
+ test "traces structs" do
+ compile_string("""
+ %URI{}
+ """)
+
+ assert_receive {:struct_expansion, meta, URI}
+ assert meta[:line] == 1
+ assert meta[:column] == 1
+ end
+
+ test "traces remote" do
+ compile_string("""
+ require Integer
+ true = Integer.is_odd(1)
+ {1, ""} = Integer.parse("1")
+ """)
+
+ assert_receive {:remote_macro, meta, Integer, :is_odd, 1}
+ assert meta[:line] == 2
+ assert meta[:column] == 15
+
+ assert_receive {:remote_function, meta, Integer, :parse, 1}
+ assert meta[:line] == 3
+ assert meta[:column] == 18
+ end
+
+ test "traces remote via captures" do
+ compile_string("""
+ require Integer
+ &Integer.is_odd/1
+ &Integer.parse/1
+ """)
+
+ assert_receive {:remote_macro, meta, Integer, :is_odd, 1}
+ assert meta[:line] == 2
+ assert meta[:column] == 1
+
+ assert_receive {:remote_function, meta, Integer, :parse, 1}
+ assert meta[:line] == 3
+ assert meta[:column] == 1
+ end
+
+ test "traces locals" do
+ compile_string("""
+ defmodule Sample do
+ defmacro foo(arg), do: arg
+ def bar(arg), do: arg
+ def baz(arg), do: foo(arg) + bar(arg)
+ end
+ """)
+
+ assert_receive {:local_macro, meta, :foo, 1}
+ assert meta[:line] == 4
+ assert meta[:column] == 21
+
+ assert_receive {:local_function, meta, :bar, 1}
+ assert meta[:line] == 4
+ assert meta[:column] == 32
+ after
+ :code.purge(Sample)
+ :code.delete(Sample)
+ end
+
+ test "traces locals with capture" do
+ compile_string("""
+ defmodule Sample do
+ defmacro foo(arg), do: arg
+ def bar(arg), do: arg
+ def baz(_), do: {&foo/1, &bar/1}
+ end
+ """)
+
+ assert_receive {:local_macro, meta, :foo, 1}
+ assert meta[:line] == 4
+ assert meta[:column] == 20
+
+ assert_receive {:local_function, meta, :bar, 1}
+ assert meta[:line] == 4
+ assert meta[:column] == 28
+ after
+ :code.purge(Sample)
+ :code.delete(Sample)
+ end
+end
diff --git a/lib/elixir/test/elixir/module/checker_test.exs b/lib/elixir/test/elixir/module/checker_test.exs
index 8fa268499..8a507d505 100644
--- a/lib/elixir/test/elixir/module/checker_test.exs
+++ b/lib/elixir/test/elixir/module/checker_test.exs
@@ -486,7 +486,7 @@ defmodule Module.CheckerTest do
end
test "excludes global no_warn_undefined" do
- no_warn_undefined = Code.compiler_option(:no_warn_undefined)
+ no_warn_undefined = Code.get_compiler_option(:no_warn_undefined)
try do
Code.compiler_options(
diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex
index 5c19f97c3..daaea6b08 100644
--- a/lib/mix/lib/mix/compilers/elixir.ex
+++ b/lib/mix/lib/mix/compilers/elixir.ex
@@ -363,7 +363,7 @@ defmodule Mix.Compilers.Elixir do
{source, sources} = List.keytake(sources, file, source(:source))
{compile_references, struct_references, runtime_references} =
- Kernel.LexicalTracker.remote_references(lexical)
+ Kernel.LexicalTracker.alias_references(lexical)
compile_references =
Enum.reject(compile_references, &match?("elixir_" <> _, Atom.to_string(&1)))
diff --git a/lib/mix/lib/mix/compilers/test.ex b/lib/mix/lib/mix/compilers/test.ex
index e1419c8d5..2e5761bd8 100644
--- a/lib/mix/lib/mix/compilers/test.ex
+++ b/lib/mix/lib/mix/compilers/test.ex
@@ -263,7 +263,7 @@ defmodule Mix.Compilers.Test do
{source, sources} = List.keytake(sources, file, source(:source))
{compile_references, struct_references, runtime_references} =
- Kernel.LexicalTracker.remote_references(lexical)
+ Kernel.LexicalTracker.alias_references(lexical)
source =
source(
diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex
index 08dc46b6c..1d82e3688 100644
--- a/lib/mix/lib/mix/tasks/compile.elixir.ex
+++ b/lib/mix/lib/mix/tasks/compile.elixir.ex
@@ -17,6 +17,7 @@ defmodule Mix.Tasks.Compile.Elixir do
## Command line options
+ * `--verbose` - prints each file being compiled
* `--force` - forces compilation regardless of modification times
* `--docs` (`--no-docs`) - attaches (or not) documentation to compiled modules
* `--debug-info` (`--no-debug-info`) - attaches (or not) debug info to compiled modules
@@ -26,6 +27,7 @@ defmodule Mix.Tasks.Compile.Elixir do
* `--long-compilation-threshold N` - sets the "long compilation" threshold
(in seconds) to `N` (see the docs for `Kernel.ParallelCompiler.compile/2`)
* `--all-warnings` - prints warnings even from files that do not need to be recompiled
+ * `--tracer` - adds a compiler tracer in addition to any specified in the `mix.exs`file
## Configuration
@@ -33,10 +35,9 @@ defmodule Mix.Tasks.Compile.Elixir do
Defaults to `["lib"]`.
* `:elixirc_options` - compilation options that apply to Elixir's compiler.
- They are the same as the command line options listed above. They must be specified
- as atoms and use underscores instead of dashes (for example, `:debug_info`). These
- options can always be overridden from the command line and they have the same defaults
- as their command line counterparts, as documented above.
+ See `Code.put_compiler_option/2` for a complete list of options. These
+ options are often overridable from the command line using the switches
+ above.
"""
@@ -48,7 +49,8 @@ defmodule Mix.Tasks.Compile.Elixir do
debug_info: :boolean,
verbose: :boolean,
long_compilation_threshold: :integer,
- all_warnings: :boolean
+ all_warnings: :boolean,
+ tracer: :keep
]
@impl true
@@ -67,14 +69,17 @@ defmodule Mix.Tasks.Compile.Elixir do
configs = [Mix.Project.config_mtime() | Mix.Tasks.Compile.Erlang.manifests()]
force = opts[:force] || Mix.Utils.stale?(configs, [manifest])
- opts = Keyword.merge(project[:elixirc_options] || [], opts)
- opts = xref_exclude_opts(opts, project)
+ opts =
+ (project[:elixirc_options] || [])
+ |> Keyword.merge(opts)
+ |> xref_exclude_opts(project)
+ |> merge_tracers()
+
Mix.Compilers.Elixir.compile(manifest, srcs, dest, [:ex], force, opts)
end
@impl true
def manifests, do: [manifest()]
-
defp manifest, do: Path.join(Mix.Project.manifest_path(), @manifest)
@impl true
@@ -83,7 +88,7 @@ defmodule Mix.Tasks.Compile.Elixir do
Mix.Compilers.Elixir.clean(manifest(), dest)
end
- # TODO: Deprecate project[:xref][:exclude] in v1.11
+ # TODO: Deprecate project[:xref][:exclude] in v1.14
defp xref_exclude_opts(opts, project) do
exclude = List.wrap(project[:xref][:exclude])
@@ -93,4 +98,15 @@ defmodule Mix.Tasks.Compile.Elixir do
Keyword.update(opts, :no_warn_undefined, exclude, &(List.wrap(&1) ++ exclude))
end
end
+
+ defp merge_tracers(opts) do
+ case Keyword.pop_values(opts, :tracer) do
+ {[], opts} ->
+ opts
+
+ {tracers, opts} ->
+ tracers = Enum.map(tracers, &Module.concat([&1]))
+ Keyword.update(opts, :tracers, tracers, &(tracers ++ &1))
+ end
+ end
end
diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs
index bf38f7dc7..b403e5128 100644
--- a/lib/mix/test/mix/tasks/compile.elixir_test.exs
+++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs
@@ -10,6 +10,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do
:ok
end
+ def trace(event, _e) do
+ send(__MODULE__, event)
+ :ok
+ end
+
@elixir_otp_version {System.version(), :erlang.system_info(:otp_release)}
test "compiles a project without per environment build" do
@@ -40,6 +45,18 @@ defmodule Mix.Tasks.Compile.ElixirTest do
end)
end
+ test "compiles a project with custom tracer" do
+ Process.register(self(), __MODULE__)
+
+ in_fixture("no_mixfile", fn ->
+ Mix.Tasks.Compile.Elixir.run(["--tracer", "Mix.Tasks.Compile.ElixirTest"])
+ assert_received {:alias_reference, _meta, A}
+ assert_received {:alias_reference, _meta, B}
+ end)
+ after
+ Code.put_compiler_option(:tracers, [])
+ end
+
test "recompiles project if Elixir version changed" do
in_fixture("no_mixfile", fn ->
Mix.Tasks.Compile.run([])
@@ -449,7 +466,6 @@ defmodule Mix.Tasks.Compile.ElixirTest do
""")
# First compilation should print unused variable warning
-
assert capture_io(:stderr, fn ->
Mix.Tasks.Compile.Elixir.run([]) == :ok
end) =~ "variable \"unused\" is unused"