Skip to content

Commit 81c5257

Browse files
authored
Merge pull request #1116 from elixir-lsp/expand
[WIP] Expand AST using Macro.Env API
2 parents a4cc1df + d1eb9f6 commit 81c5257

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1821
-1383
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,6 @@ jobs:
1616
fail-fast: false
1717
matrix:
1818
include:
19-
- elixir: 1.12.x
20-
otp: 22.x
21-
tests_may_fail: false
22-
- elixir: 1.12.x
23-
otp: 23.x
24-
tests_may_fail: false
25-
- elixir: 1.12.x
26-
otp: 24.x
27-
tests_may_fail: false
2819
- elixir: 1.13.x
2920
otp: 22.x
3021
tests_may_fail: false
@@ -98,12 +89,6 @@ jobs:
9889
fail-fast: false
9990
matrix:
10091
include:
101-
- elixir: 1.12.x
102-
otp: 22.x
103-
- elixir: 1.12.x
104-
otp: 23.x
105-
- elixir: 1.12.x
106-
otp: 24.x
10792
- elixir: 1.13.x
10893
otp: 22.x
10994
- elixir: 1.13.x
@@ -165,8 +150,8 @@ jobs:
165150
strategy:
166151
matrix:
167152
include:
168-
- elixir: 1.15.x
169-
otp: 25.x
153+
- elixir: 1.17.x
154+
otp: 27.x
170155
steps:
171156
- uses: actions/checkout@v4
172157
- uses: erlef/setup-beam@v1

apps/debug_adapter/lib/debug_adapter/breakpoint_condition.ex

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,27 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
1616
@spec register_condition(
1717
module,
1818
module,
19-
[non_neg_integer],
19+
non_neg_integer,
2020
String.t(),
2121
String.t() | nil,
22-
non_neg_integer
22+
String.t()
2323
) ::
2424
{:ok, {module, atom}} | {:error, :limit_reached}
25-
def register_condition(name \\ __MODULE__, module, lines, condition, log_message, hit_count) do
25+
def register_condition(name \\ __MODULE__, module, line, env, condition, log_message, hit_count) do
2626
GenServer.call(
2727
name,
28-
{:register_condition, {module, lines}, condition, log_message, hit_count}
28+
{:register_condition, {module, line}, env, condition, log_message, hit_count}
2929
)
3030
end
3131

32-
@spec unregister_condition(module, module, [non_neg_integer]) :: :ok
33-
def unregister_condition(name \\ __MODULE__, module, lines) do
34-
GenServer.cast(name, {:unregister_condition, {module, lines}})
32+
@spec unregister_condition(module, module, non_neg_integer) :: :ok
33+
def unregister_condition(name \\ __MODULE__, module, line) do
34+
GenServer.cast(name, {:unregister_condition, {module, line}})
3535
end
3636

37-
@spec has_condition?(module, module, [non_neg_integer]) :: boolean
38-
def has_condition?(name \\ __MODULE__, module, lines) do
39-
GenServer.call(name, {:has_condition?, {module, lines}})
37+
@spec has_condition?(module, module, non_neg_integer) :: boolean
38+
def has_condition?(name \\ __MODULE__, module, line) do
39+
GenServer.call(name, {:has_condition?, {module, line}})
4040
end
4141

4242
@spec get_condition(module, non_neg_integer) ::
@@ -96,7 +96,7 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
9696

9797
@impl GenServer
9898
def handle_call(
99-
{:register_condition, key, condition, log_message, hit_count},
99+
{:register_condition, key, env, condition, log_message, hit_count},
100100
_from,
101101
%{free: free, conditions: conditions} = state
102102
) do
@@ -111,7 +111,7 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
111111
state
112112
| free: rest,
113113
conditions:
114-
conditions |> Map.put(key, {number, {condition, log_message, hit_count}})
114+
conditions |> Map.put(key, {number, {env, condition, log_message, hit_count}})
115115
}
116116

117117
{:reply, {:ok, {__MODULE__, :"check_#{number}"}}, state}
@@ -120,7 +120,8 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
120120
{number, _old_condition} ->
121121
state = %{
122122
state
123-
| conditions: conditions |> Map.put(key, {number, {condition, log_message, hit_count}})
123+
| conditions:
124+
conditions |> Map.put(key, {number, {env, condition, log_message, hit_count}})
124125
}
125126

126127
{:reply, {:ok, {__MODULE__, :"check_#{number}"}}, state}
@@ -132,11 +133,11 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
132133
end
133134

134135
def handle_call({:get_condition, number}, _from, %{conditions: conditions, hits: hits} = state) do
135-
{condition, log_message, hit_count} =
136+
{env, condition, log_message, hit_count} =
136137
conditions |> Map.values() |> Enum.find(fn {n, _c} -> n == number end) |> elem(1)
137138

138139
hits = hits |> Map.get(number, 0)
139-
{:reply, {condition, log_message, hit_count, hits}, state}
140+
{:reply, {env, condition, log_message, hit_count, hits}, state}
140141
end
141142

142143
def handle_call(:clear, _from, _state) do
@@ -177,14 +178,20 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
177178
for i <- @range do
178179
@spec unquote(:"check_#{i}")(term) :: boolean
179180
def unquote(:"check_#{i}")(binding) do
180-
{condition, log_message, hit_count, hits} = get_condition(unquote(i))
181+
{env, condition, log_message, hit_count_condition, hits} = get_condition(unquote(i))
181182
elixir_binding = binding |> ElixirLS.DebugAdapter.Binding.to_elixir_variable_names()
182-
result = eval_condition(condition, elixir_binding)
183+
result = eval_condition(condition, elixir_binding, env)
183184

184185
result =
185186
if result do
186187
register_hit(unquote(i))
187188
# do not break if hit count not reached
189+
# the spec requires:
190+
# If both this property and `condition` are specified, `hitCondition` should
191+
# be evaluated only if the `condition` is met, and the debug adapter should
192+
# stop only if both conditions are met.
193+
194+
hit_count = eval_hit_condition(hit_count_condition, elixir_binding, env)
188195
hits + 1 > hit_count
189196
else
190197
result
@@ -194,20 +201,22 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
194201
# Debug Adapter Protocol:
195202
# If this attribute exists and is non-empty, the backend must not 'break' (stop)
196203
# but log the message instead. Expressions within {} are interpolated.
197-
Output.debugger_console(interpolate(log_message, elixir_binding))
204+
# If either `hitCondition` or `condition` is specified, then the message
205+
# should only be logged if those conditions are met.
206+
Output.debugger_console(interpolate(log_message, elixir_binding, env))
198207
false
199208
else
200209
result
201210
end
202211
end
203212
end
204213

205-
@spec eval_condition(String.t(), keyword) :: boolean
206-
def eval_condition("true", _binding), do: true
214+
@spec eval_condition(String.t(), keyword, Macro.Env.t()) :: boolean
215+
def eval_condition("true", _binding, _env), do: true
207216

208-
def eval_condition(condition, elixir_binding) do
217+
def eval_condition(condition, elixir_binding, env) do
209218
try do
210-
{term, _bindings} = Code.eval_string(condition, elixir_binding)
219+
{term, _bindings} = Code.eval_string(condition, elixir_binding, env)
211220
if term, do: true, else: false
212221
catch
213222
kind, error ->
@@ -219,9 +228,29 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
219228
end
220229
end
221230

222-
def eval_string(expression, elixir_binding) do
231+
@spec eval_hit_condition(String.t(), keyword, Macro.Env.t()) :: number
232+
def eval_hit_condition("0", _binding, _env), do: 0
233+
234+
def eval_hit_condition(condition, elixir_binding, env) do
235+
try do
236+
{term, _bindings} = Code.eval_string(condition, elixir_binding, env)
237+
238+
if is_number(term) do
239+
term
240+
else
241+
raise "Hit count evaluated to non number #{inspect(term)}"
242+
end
243+
catch
244+
kind, error ->
245+
Output.debugger_important("Error in hit count: " <> Exception.format_banner(kind, error))
246+
247+
0
248+
end
249+
end
250+
251+
def eval_string(expression, elixir_binding, env) do
223252
try do
224-
{term, _bindings} = Code.eval_string(expression, elixir_binding)
253+
{term, _bindings} = Code.eval_string(expression, elixir_binding, env)
225254
to_string(term)
226255
catch
227256
kind, error ->
@@ -233,39 +262,39 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
233262
end
234263
end
235264

236-
def interpolate(format_string, elixir_binding) do
237-
interpolate(format_string, [], elixir_binding)
265+
def interpolate(format_string, elixir_binding, env) do
266+
interpolate(format_string, [], elixir_binding, env)
238267
|> Enum.reverse()
239268
|> IO.iodata_to_binary()
240269
end
241270

242-
def interpolate(<<>>, acc, _elixir_binding), do: acc
271+
def interpolate(<<>>, acc, _elixir_binding, _env), do: acc
243272

244-
def interpolate(<<"\\{", rest::binary>>, acc, elixir_binding),
245-
do: interpolate(rest, ["{" | acc], elixir_binding)
273+
def interpolate(<<"\\{", rest::binary>>, acc, elixir_binding, env),
274+
do: interpolate(rest, ["{" | acc], elixir_binding, env)
246275

247-
def interpolate(<<"\\}", rest::binary>>, acc, elixir_binding),
248-
do: interpolate(rest, ["}" | acc], elixir_binding)
276+
def interpolate(<<"\\}", rest::binary>>, acc, elixir_binding, env),
277+
do: interpolate(rest, ["}" | acc], elixir_binding, env)
249278

250-
def interpolate(<<"{", rest::binary>>, acc, elixir_binding) do
279+
def interpolate(<<"{", rest::binary>>, acc, elixir_binding, env) do
251280
case parse_expression(rest, []) do
252281
{:ok, expression_iolist, expression_rest} ->
253282
expression =
254283
expression_iolist
255284
|> Enum.reverse()
256285
|> IO.iodata_to_binary()
257286

258-
eval_result = eval_string(expression, elixir_binding)
259-
interpolate(expression_rest, [eval_result | acc], elixir_binding)
287+
eval_result = eval_string(expression, elixir_binding, env)
288+
interpolate(expression_rest, [eval_result | acc], elixir_binding, env)
260289

261290
:error ->
262291
Output.debugger_important("Log message has unpaired or nested `{}`")
263292
acc
264293
end
265294
end
266295

267-
def interpolate(<<char::binary-size(1), rest::binary>>, acc, elixir_binding),
268-
do: interpolate(rest, [char | acc], elixir_binding)
296+
def interpolate(<<char::binary-size(1), rest::binary>>, acc, elixir_binding, env),
297+
do: interpolate(rest, [char | acc], elixir_binding, env)
269298

270299
def parse_expression(<<>>, _acc), do: :error
271300
def parse_expression(<<"\\{", rest::binary>>, acc), do: parse_expression(rest, ["{" | acc])
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
defmodule ElixirLS.DebugAdapter.Code do
2+
if Version.match?(System.version(), ">= 1.14.0-dev") do
3+
defdelegate env_for_eval(env), to: Code
4+
else
5+
def env_for_eval(%{lexical_tracker: pid} = env) do
6+
new_env = %{
7+
env
8+
| context: nil,
9+
context_modules: [],
10+
macro_aliases: [],
11+
versioned_vars: %{}
12+
}
13+
14+
if is_pid(pid) do
15+
if Process.alive?(pid) do
16+
new_env
17+
else
18+
IO.warn("""
19+
an __ENV__ with outdated compilation information was given to eval, \
20+
call Macro.Env.prune_compile_info/1 to prune it
21+
""")
22+
23+
%{new_env | lexical_tracker: nil, tracers: []}
24+
end
25+
else
26+
%{new_env | tracers: []}
27+
end
28+
end
29+
30+
def env_for_eval(opts) when is_list(opts) do
31+
env = :elixir_env.new()
32+
33+
line =
34+
case Keyword.get(opts, :line) do
35+
line_opt when is_integer(line_opt) -> line_opt
36+
nil -> Map.get(env, :line)
37+
end
38+
39+
file =
40+
case Keyword.get(opts, :file) do
41+
file_opt when is_binary(file_opt) -> file_opt
42+
nil -> Map.get(env, :file)
43+
end
44+
45+
module =
46+
case Keyword.get(opts, :module) do
47+
module_opt when is_atom(module_opt) -> module_opt
48+
nil -> nil
49+
end
50+
51+
fa =
52+
case Keyword.get(opts, :function) do
53+
{function, arity} when is_atom(function) and is_integer(arity) -> {function, arity}
54+
nil -> nil
55+
end
56+
57+
temp_tracers =
58+
case Keyword.get(opts, :tracers) do
59+
tracers_opt when is_list(tracers_opt) -> tracers_opt
60+
nil -> []
61+
end
62+
63+
aliases =
64+
case Keyword.get(opts, :aliases) do
65+
aliases_opt when is_list(aliases_opt) ->
66+
IO.warn(":aliases option in eval is deprecated")
67+
aliases_opt
68+
69+
nil ->
70+
Map.get(env, :aliases)
71+
end
72+
73+
requires =
74+
case Keyword.get(opts, :requires) do
75+
requires_opt when is_list(requires_opt) ->
76+
IO.warn(":requires option in eval is deprecated")
77+
MapSet.new(requires_opt)
78+
79+
nil ->
80+
Map.get(env, :requires)
81+
end
82+
83+
functions =
84+
case Keyword.get(opts, :functions) do
85+
functions_opt when is_list(functions_opt) ->
86+
IO.warn(":functions option in eval is deprecated")
87+
functions_opt
88+
89+
nil ->
90+
Map.get(env, :functions)
91+
end
92+
93+
macros =
94+
case Keyword.get(opts, :macros) do
95+
macros_opt when is_list(macros_opt) ->
96+
IO.warn(":macros option in eval is deprecated")
97+
macros_opt
98+
99+
nil ->
100+
Map.get(env, :macros)
101+
end
102+
103+
{lexical_tracker, tracers} =
104+
case Keyword.get(opts, :lexical_tracker) do
105+
pid when is_pid(pid) ->
106+
IO.warn(":lexical_tracker option in eval is deprecated")
107+
108+
if Process.alive?(pid) do
109+
{pid, temp_tracers}
110+
else
111+
{nil, []}
112+
end
113+
114+
nil ->
115+
IO.warn(":lexical_tracker option in eval is deprecated")
116+
{nil, []}
117+
118+
_ ->
119+
{nil, temp_tracers}
120+
end
121+
122+
%{
123+
env
124+
| file: file,
125+
module: module,
126+
function: fa,
127+
tracers: tracers,
128+
macros: macros,
129+
functions: functions,
130+
lexical_tracker: lexical_tracker,
131+
requires: requires,
132+
aliases: aliases,
133+
line: line
134+
}
135+
end
136+
end
137+
end

0 commit comments

Comments
 (0)