summaryrefslogtreecommitdiff
path: root/lib/elixir/src/elixir_env.erl
blob: f7f81e7650ce5b81c907f0e70196bbe927ef8f14 (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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
-module(elixir_env).
-include("elixir.hrl").
-export([
  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
]).

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
    unused_vars => #{},                    %% a map with unused variables
    current_vars => #{},                   %% a map with current variables
    prematch_vars => warn,                 %% behaviour outside and inside matches
    lexical_tracker => nil,                %% holds the lexical tracker PID
    contextual_vars => []}.                %% holds available contextual variables

linify({Line, Env}) ->
  Env#{line := Line};
linify(#{} = Env) ->
  Env.

with_vars(Env, Vars) ->
  CurrentVars = maps:from_list([{Var, {0, term}} || Var <- Vars]),
  Env#{vars := Vars, current_vars := CurrentVars}.

env_to_scope(#{context := Context}) ->
  #elixir_erl{context=Context}.

env_to_scope_with_vars(Env, Vars) ->
  Map = maps:from_list(Vars),
  (env_to_scope(Env))#elixir_erl{
    vars=Map, counter=#{'_' => map_size(Map)}
  }.

reset_vars(Env) ->
  Env#{vars := [], current_vars := #{}, unused_vars := #{}}.

%% SCOPE MERGING

%% Receives two scopes and return a new scope based on the second
%% with their variables merged.
%% Unrolled for performance reasons.
mergev(#{unused_vars := U1, current_vars := C1},
       #{unused_vars := U2, current_vars := C2} = E2) ->
  if
    C1 =/= C2 ->
      if
        U1 =/= U2 ->
          C = merge_vars(C1, C2),
          E2#{vars := maps:keys(C), unused_vars := merge_vars(U1, U2), current_vars := C};
        true ->
          C = merge_vars(C1, C2),
          E2#{vars := maps:keys(C), current_vars := C}
      end;

    U1 =/= U2 ->
      E2#{unused_vars := merge_vars(U1, U2)};

    true ->
      E2
  end.

%% Receives two scopes and return the later scope
%% keeping the variables from the first (imports
%% and everything else are passed forward).
%% Unrolled for performance reasons.
mergea(#{unused_vars := U1, current_vars := C1, vars := V1},
       #{unused_vars := U2, current_vars := C2} = E2) ->
  if
    C1 =/= C2 ->
      if
        U1 =/= U2 ->
          E2#{vars := V1, unused_vars := U1, current_vars := C1};
        true ->
          E2#{vars := V1, current_vars := C1}
      end;
    U1 =/= U2 ->
      E2#{unused_vars := U1};
    true ->
      E2
  end.

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

check_unused_vars(#{unused_vars := Unused} = E) ->
  [elixir_errors:form_warn([{line, Line}], ?key(E, file), ?MODULE, {unused_var, Name, false}) ||
    {{{Name, _}, _}, Line} <- maps:to_list(Unused), Line /= false, not_underscored(Name)],
  E.

merge_and_check_unused_vars(#{unused_vars := Unused} = E, #{unused_vars := ClauseUnused}) ->
  E#{unused_vars := merge_and_check_unused_vars(Unused, ClauseUnused, E)}.

merge_and_check_unused_vars(Unused, ClauseUnused, E) ->
  maps:fold(fun(Key, ClauseValue, Acc) ->
    case ClauseValue of
      %% The variable was used...
      false ->
        case Acc of
          %% So we propagate if it was not yet used
          #{Key := Value} when Value /= false ->
            Acc#{Key := false};

          %% Otherwise we don't know it or it was already used
          _ ->
            Acc
        end;

      %% The variable was not used...
      _ ->
        case Acc of
          %% If we know it, there is nothing to propagate
          #{Key := _} ->
            Acc;

          %% Otherwise we must warn
          _ ->
            {{Name, _} = Pair, _} = Key,

            case not_underscored(Name) of
              true ->
                IsShadowing = maps:is_key(Pair, ?key(E, current_vars)),
                Warn = {unused_var, Name, IsShadowing},
                elixir_errors:form_warn([{line, ClauseValue}], ?key(E, file), ?MODULE, Warn);

              false ->
                ok
            end,

            Acc
        end
    end
  end, Unused, ClauseUnused).

not_underscored(Name) ->
  case atom_to_list(Name) of
    "_" ++ _ -> false;
    _ -> true
  end.

format_error({unused_var, Name, false}) ->
  io_lib:format("variable \"~ts\" is unused (if the variable is not meant to be used, prefix it with an underscore)", [Name]);

format_error({unused_var, Name, true}) ->
  io_lib:format("variable \"~ts\" is unused\n\n"
                "Note variables defined inside case, cond, fn, if and similar do not affect "
                "variables defined outside of the construct. Instead you have to explicitly "
                "return those values. For example:\n\n"
                "    if some_condition? do\n"
                "      atom = :one\n"
                "    else\n"
                "      atom = :two\n"
                "    end\n\n"
                "should be written as\n\n"
                "    atom =\n"
                "      if some_condition? do\n"
                "        :one\n"
                "      else\n"
                "        :two\n"
                "      end\n\n"
                "Unused variable \"~ts\" found at:", [Name, Name]).