summaryrefslogtreecommitdiff
path: root/lib/elixir/src/elixir_env.erl
blob: fe6d287c9def7c2f1bffb8ed7ee65e5e42c932d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
-module(elixir_env).
-include("elixir.hrl").
-export([
  new/0, to_caller/1, with_vars/2, reset_vars/1, env_to_ex/1, set_prematch_from_config/1,
  reset_unused_vars/1, check_unused_vars/2, merge_and_check_unused_vars/3,
  trace/2, format_error/1,
  reset_read/2, prepare_write/1, close_write/2
]).

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
    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
    versioned_vars => #{},                            %% a map of vars with their latest versions
    lexical_tracker => nil,                           %% lexical tracker PID
    tracers => []                                     %% available compilation tracers
  }.

trace(Event, #{tracers := Tracers} = E) ->
  [ok = Tracer:trace(Event, E) || Tracer <- Tracers],
  ok.

to_caller({Line, #elixir_ex{vars={Read, _}}, Env}) ->
  Env#{line := Line, versioned_vars := Read};
to_caller(#{} = Env) ->
  Env.

with_vars(Env, Vars) when is_list(Vars) ->
  {ReversedVars, _} =
    lists:foldl(fun(Var, {Acc, I}) -> {[{Var, I} | Acc], I + 1} end, {[], 0}, Vars),
  Env#{versioned_vars := maps:from_list(ReversedVars)};
with_vars(Env, #{} = Vars) ->
  Env#{versioned_vars := Vars}.

reset_vars(Env) ->
  Env#{versioned_vars := #{}}.

set_prematch_from_config(#elixir_ex{} = S) ->
  S#elixir_ex{prematch=elixir_config:get(on_undefined_variable)}.

%% CONVERSIONS

env_to_ex(#{context := match, versioned_vars := Vars}) ->
  Counter = map_size(Vars),
  #elixir_ex{prematch={Vars, Counter}, vars={Vars, false}, unused={#{}, Counter}};
env_to_ex(#{versioned_vars := Vars}) ->
  set_prematch_from_config(#elixir_ex{vars={Vars, false}, unused={#{}, map_size(Vars)}}).

%% VAR HANDLING

reset_read(#elixir_ex{vars={_, Write}} = S, #elixir_ex{vars={Read, _}}) ->
  S#elixir_ex{vars={Read, Write}}.

prepare_write(#elixir_ex{vars={Read, _}} = S) ->
  S#elixir_ex{vars={Read, Read}}.

close_write(#elixir_ex{vars={_Read, Write}} = S, #elixir_ex{vars={_, false}}) ->
  S#elixir_ex{vars={Write, false}};
close_write(#elixir_ex{vars={_Read, Write}} = S, #elixir_ex{vars={_, UpperWrite}}) ->
  S#elixir_ex{vars={Write, merge_vars(UpperWrite, Write)}}.

merge_vars(V, V) ->
  V;
merge_vars(V1, V2) ->
  maps:fold(fun(K, M2, Acc) ->
    case Acc of
      #{K := M1} when M1 >= M2 -> Acc;
      _ -> Acc#{K => M2}
    end
  end, V1, V2).

%% UNUSED VARS

reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) ->
  S#elixir_ex{unused={#{}, Version}}.

check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) ->
  [elixir_errors:file_warn([{line, Line}], E, ?MODULE, {unused_var, Name, Overridden}) ||
    {{{Name, nil}, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
  E.

merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _Version}}, E) ->
  #elixir_ex{unused={ClauseUnused, Version}} = S,
  NewUnused = merge_and_check_unused_vars(Read, Unused, ClauseUnused, E),
  S#elixir_ex{unused={NewUnused, Version}, vars={Read, Write}}.

merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
  maps:fold(fun
    ({Var, Count} = Key, false, Acc) ->
      case Current of
        #{Var := CurrentCount} when Count =< CurrentCount ->
          %% The parent knows it, so we have to propagate it was used up.
          Acc#{Key => false};

        #{} ->
          Acc
      end;

    ({{Name, Kind}, _Count}, {Line, Overridden}, Acc) ->
      case (Kind == nil) andalso is_unused_var(Name) of
        true ->
          Warn = {unused_var, Name, Overridden},
          elixir_errors:file_warn([{line, Line}], E, ?MODULE, Warn);

        false ->
          ok
      end,

      Acc
  end, Unused, ClauseUnused).

is_unused_var(Name) ->
  case atom_to_list(Name) of
    "_" ++ Rest -> is_compiler_var(Rest);
    _ -> true
  end.

is_compiler_var([$_]) -> true;
is_compiler_var([Var | Rest]) when Var =:= $_; Var >= $A, Var =< $Z -> is_compiler_var(Rest);
is_compiler_var(_) -> false.

format_error({unused_var, Name, Overridden}) ->
  case atom_to_list(Name) of
    "_" ++ _ ->
      io_lib:format("unknown compiler variable \"~ts\" (expected one of __MODULE__, __ENV__, __DIR__, __CALLER__, __STACKTRACE__)", [Name]);
    _ when Overridden ->
      io_lib:format("variable \"~ts\" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)", [Name]);
    _ ->
      io_lib:format("variable \"~ts\" is unused (if the variable is not meant to be used, prefix it with an underscore)", [Name])
  end.