diff options
author | José Valim <jose.valim@plataformatec.com.br> | 2019-08-02 13:35:12 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-02 13:35:12 +0200 |
commit | b08593b9d1b82e65035abc9bf074ea47dc5817b1 (patch) | |
tree | 722433438740cab97e11776390e1552155036cff | |
parent | d82616786567d497fa08f62d4f3dc2a7d2134306 (diff) | |
download | elixir-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.
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" |