1defmodule Macro.Env do
2  @moduledoc """
3  A struct that holds compile time environment information.
4
5  The current environment can be accessed at any time as
6  `__ENV__/0`. Inside macros, the caller environment can be
7  accessed as `__CALLER__/0`.
8
9  An instance of `Macro.Env` must not be modified by hand. If you need to
10  create a custom environment to pass to `Code.eval_quoted/3`, use the
11  following trick:
12
13      def make_custom_env do
14        import SomeModule, only: [some_function: 2]
15        alias A.B.C
16        __ENV__
17      end
18
19  You may then call `make_custom_env()` to get a struct with the desired
20  imports and aliases included.
21
22  It contains the following fields:
23
24    * `context` - the context of the environment; it can be `nil`
25      (default context), `:guard` (inside a guard) or `:match` (inside a match)
26    * `context_modules` - a list of modules defined in the current context
27    * `file` - the current file name as a binary
28    * `function` - a tuple as `{atom, integer}`, where the first
29      element is the function name and the second its arity; returns
30      `nil` if not inside a function
31    * `line` - the current line as an integer
32    * `module` - the current module name
33
34  The following fields are private to Elixir's macro expansion mechanism and
35  must not be accessed directly:
36
37    * `aliases`
38    * `functions`
39    * `macro_aliases`
40    * `macros`
41    * `lexical_tracker`
42    * `requires`
43    * `tracers`
44    * `versioned_vars`
45
46  """
47
48  @type context :: :match | :guard | nil
49  @type context_modules :: [module]
50  @type file :: binary
51  @type line :: non_neg_integer
52  @type name_arity :: {atom, arity}
53  @type variable :: {atom, atom | term}
54
55  @typep aliases :: [{module, module}]
56  @typep functions :: [{module, [name_arity]}]
57  @typep lexical_tracker :: pid | nil
58  @typep macro_aliases :: [{module, {term, module}}]
59  @typep macros :: [{module, [name_arity]}]
60  @typep requires :: [module]
61  @typep tracers :: [module]
62  @typep versioned_vars :: %{optional(variable) => var_version :: non_neg_integer}
63
64  @type t :: %{
65          __struct__: __MODULE__,
66          aliases: aliases,
67          context: context,
68          context_modules: context_modules,
69          file: file,
70          function: name_arity | nil,
71          functions: functions,
72          lexical_tracker: lexical_tracker,
73          line: line,
74          macro_aliases: macro_aliases,
75          macros: macros,
76          module: module,
77          requires: requires,
78          tracers: tracers,
79          versioned_vars: versioned_vars
80        }
81
82  # Define the __struct__ callbacks by hand for bootstrap reasons.
83  @doc false
84  def __struct__ do
85    %{
86      __struct__: __MODULE__,
87      aliases: [],
88      context: nil,
89      context_modules: [],
90      file: "nofile",
91      function: nil,
92      functions: [],
93      lexical_tracker: nil,
94      line: 0,
95      macro_aliases: [],
96      macros: [],
97      module: nil,
98      requires: [],
99      tracers: [],
100      versioned_vars: %{}
101    }
102  end
103
104  @doc false
105  def __struct__(kv) do
106    Enum.reduce(kv, __struct__(), fn {k, v}, acc -> :maps.update(k, v, acc) end)
107  end
108
109  @doc """
110  Returns a list of variables in the current environment.
111
112  Each variable is identified by a tuple of two elements,
113  where the first element is the variable name as an atom
114  and the second element is its context, which may be an
115  atom or an integer.
116  """
117  @doc since: "1.7.0"
118  @spec vars(t) :: [variable]
119  def vars(env)
120
121  def vars(%{__struct__: Macro.Env, versioned_vars: vars}) do
122    Map.keys(vars)
123  end
124
125  @doc """
126  Checks if a variable belongs to the environment.
127
128  ## Examples
129
130      iex> x = 13
131      iex> x
132      13
133      iex> Macro.Env.has_var?(__ENV__, {:x, nil})
134      true
135      iex> Macro.Env.has_var?(__ENV__, {:unknown, nil})
136      false
137
138  """
139  @doc since: "1.7.0"
140  @spec has_var?(t, variable) :: boolean()
141  def has_var?(env, var)
142
143  def has_var?(%{__struct__: Macro.Env, versioned_vars: vars}, var) do
144    Map.has_key?(vars, var)
145  end
146
147  @doc """
148  Returns a keyword list containing the file and line
149  information as keys.
150  """
151  @spec location(t) :: keyword
152  def location(env)
153
154  def location(%{__struct__: Macro.Env, file: file, line: line}) do
155    [file: file, line: line]
156  end
157
158  @doc """
159  Fetches the alias for the given atom.
160
161  Returns `{:ok, alias}` if the alias exists, `:error`
162  otherwise.
163
164  ## Examples
165
166      iex> alias Foo.Bar, as: Baz
167      iex> Baz
168      Foo.Bar
169      iex> Macro.Env.fetch_alias(__ENV__, :Baz)
170      {:ok, Foo.Bar}
171      iex> Macro.Env.fetch_alias(__ENV__, :Unknown)
172      :error
173
174  """
175  @doc since: "1.13.0"
176  @spec fetch_alias(t, atom) :: {:ok, atom} | :error
177  def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
178    do: Keyword.fetch(aliases, :"Elixir.#{atom}")
179
180  @doc """
181  Fetches the macro alias for the given atom.
182
183  Returns `{:ok, macro_alias}` if the alias exists, `:error`
184  otherwise.
185
186  A macro alias is only used inside quoted expansion. See
187  `fetch_alias/2` for a more general example.
188  """
189  @doc since: "1.13.0"
190  @spec fetch_macro_alias(t, atom) :: {:ok, atom} | :error
191  def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
192      when is_atom(atom),
193      do: Keyword.fetch(aliases, :"Elixir.#{atom}")
194
195  @doc """
196  Returns the modules from which the given `{name, arity}` was
197  imported.
198
199  It returns a list of two element tuples in the shape of
200  `{:function | :macro, module}`. The elements in the list
201  are in no particular order and the order is not guaranteed.
202
203  ## Examples
204
205      iex> Macro.Env.lookup_import(__ENV__, {:duplicate, 2})
206      []
207      iex> import Tuple, only: [duplicate: 2], warn: false
208      iex> Macro.Env.lookup_import(__ENV__, {:duplicate, 2})
209      [{:function, Tuple}]
210      iex> import List, only: [duplicate: 2], warn: false
211      iex> Macro.Env.lookup_import(__ENV__, {:duplicate, 2})
212      [{:function, List}, {:function, Tuple}]
213
214      iex> Macro.Env.lookup_import(__ENV__, {:def, 1})
215      [{:macro, Kernel}]
216
217  """
218  @doc since: "1.13.0"
219  @spec lookup_import(t, name_arity) :: [{:function | :macro, module}]
220  def lookup_import(
221        %{__struct__: Macro.Env, functions: functions, macros: macros},
222        {name, arity} = pair
223      )
224      when is_atom(name) and is_integer(arity) do
225    f = for {mod, pairs} <- functions, :ordsets.is_element(pair, pairs), do: {:function, mod}
226    m = for {mod, pairs} <- macros, :ordsets.is_element(pair, pairs), do: {:macro, mod}
227    f ++ m
228  end
229
230  @doc """
231  Returns true if the given module has been required.
232
233  ## Examples
234
235      iex> Macro.Env.required?(__ENV__, Integer)
236      false
237      iex> require Integer
238      iex> Macro.Env.required?(__ENV__, Integer)
239      true
240
241      iex> Macro.Env.required?(__ENV__, Kernel)
242      true
243  """
244  @doc since: "1.13.0"
245  @spec required?(t, module) :: boolean
246  def required?(%{__struct__: Macro.Env, requires: requires}, mod) when is_atom(mod),
247    do: mod in requires
248
249  @doc """
250  Prepend a tracer to the list of tracers in the environment.
251
252  ## Examples
253
254      Macro.Env.prepend_tracer(__ENV__, MyCustomTracer)
255
256  """
257  @doc since: "1.13.0"
258  @spec prepend_tracer(t, module) :: t
259  def prepend_tracer(%{__struct__: Macro.Env, tracers: tracers} = env, tracer) do
260    %{env | tracers: [tracer | tracers]}
261  end
262
263  @doc """
264  Returns a `Macro.Env` in the match context.
265  """
266  @spec to_match(t) :: t
267  def to_match(%{__struct__: Macro.Env} = env) do
268    %{env | context: :match}
269  end
270
271  @doc """
272  Returns whether the compilation environment is currently
273  inside a guard.
274  """
275  @spec in_guard?(t) :: boolean
276  def in_guard?(env)
277  def in_guard?(%{__struct__: Macro.Env, context: context}), do: context == :guard
278
279  @doc """
280  Returns whether the compilation environment is currently
281  inside a match clause.
282  """
283  @spec in_match?(t) :: boolean
284  def in_match?(env)
285  def in_match?(%{__struct__: Macro.Env, context: context}), do: context == :match
286
287  @doc """
288  Returns the environment stacktrace.
289  """
290  @spec stacktrace(t) :: list
291  def stacktrace(%{__struct__: Macro.Env} = env) do
292    cond do
293      is_nil(env.module) ->
294        [{:elixir_compiler, :__FILE__, 1, relative_location(env)}]
295
296      is_nil(env.function) ->
297        [{env.module, :__MODULE__, 0, relative_location(env)}]
298
299      true ->
300        {name, arity} = env.function
301        [{env.module, name, arity, relative_location(env)}]
302    end
303  end
304
305  defp relative_location(env) do
306    [file: String.to_charlist(Path.relative_to_cwd(env.file)), line: env.line]
307  end
308end
309