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]).
|