Skip to content
This repository was archived by the owner on Oct 8, 2025. It is now read-only.

Commit 4135f6f

Browse files
committed
Refactor to use the AST
1 parent 88f5bca commit 4135f6f

File tree

2 files changed

+234
-29
lines changed

2 files changed

+234
-29
lines changed

lib/next_ls/extensions/credo_extension/code_action/remove_debugger.ex

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,103 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebugger do
22
@moduledoc false
33

44
alias GenLSP.Structures.CodeAction
5+
alias GenLSP.Structures.Position
56
alias GenLSP.Structures.Diagnostic
67
alias GenLSP.Structures.Range
78
alias GenLSP.Structures.TextEdit
89
alias GenLSP.Structures.WorkspaceEdit
10+
alias NextLS.ASTHelpers
11+
alias NextLS.EditHelpers
12+
alias Sourceror.Zipper, as: Z
13+
@line_length 121
914

10-
def new(diagnostic, _text, uri) do
11-
%Diagnostic{range: %Range{start: start}} = diagnostic
12-
13-
[
14-
%CodeAction{
15-
title: "Remove debugger",
16-
diagnostics: [diagnostic],
17-
edit: %WorkspaceEdit{
18-
changes: %{
19-
uri => [
20-
%TextEdit{
21-
new_text: "",
22-
range: %Range{
23-
start: %{start | character: 0},
24-
end: %{start | character: 0, line: start.line + 1}
15+
def new(diagnostic, text, uri) do
16+
%Diagnostic{range: range} = diagnostic
17+
18+
with {:ok, ast, comments} <- parse(text),
19+
{:ok, defm} <- ASTHelpers.get_surrounding_module(ast, range.start) do
20+
range = make_range(defm)
21+
indent = EditHelpers.get_indent(text, range.start.line)
22+
diagnostic.range.start
23+
ast_without_debugger = remove_debugger(defm, diagnostic.range.start)
24+
25+
comments =
26+
Enum.filter(comments, fn comment ->
27+
comment.line > range.start.line && comment.line <= range.end.line
28+
end)
29+
30+
to_algebra_opts = [comments: comments]
31+
doc = Code.quoted_to_algebra(ast_without_debugger, to_algebra_opts)
32+
formatted = doc |> Inspect.Algebra.format(@line_length) |> IO.iodata_to_binary()
33+
34+
[
35+
%CodeAction{
36+
title: "Remove debugger",
37+
diagnostics: [diagnostic],
38+
edit: %WorkspaceEdit{
39+
changes: %{
40+
uri => [
41+
%TextEdit{
42+
new_text: EditHelpers.add_indent_to_edit(formatted, indent),
43+
range: range
2544
}
26-
}
27-
]
45+
]
46+
}
2847
}
2948
}
30-
}
31-
]
49+
]
50+
else
51+
{:error, message} ->
52+
%GenLSP.ErrorResponse{code: ErrorCodes.parse_error(), message: inspect(message)}
53+
end
54+
end
55+
56+
defp remove_debugger(ast, position) do
57+
pos = [line: position.line + 1, column: position.character + 1]
58+
result =
59+
ast
60+
|> Z.zip()
61+
|> Z.traverse(fn tree ->
62+
node = Z.node(tree)
63+
range = Sourceror.get_range(node)
64+
65+
if matches_debug?(node, pos) &&
66+
Sourceror.compare_positions(range.start, pos) in [:lt, :eq] &&
67+
Sourceror.compare_positions(range.end, pos) in [:gt, :eq] do
68+
Z.remove(tree)
69+
else
70+
tree
71+
end
72+
end)
73+
|> Z.node()
3274
end
75+
76+
defp parse(lines) do
77+
lines
78+
|> Enum.join("\n")
79+
|> Spitfire.parse_with_comments(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
80+
|> case do
81+
{:error, ast, comments, _errors} ->
82+
{:ok, ast, comments}
83+
84+
other ->
85+
other
86+
end
87+
end
88+
89+
defp make_range(original_ast) do
90+
range = Sourceror.get_range(original_ast)
91+
92+
%Range{
93+
start: %Position{line: range.start[:line] - 1, character: range.start[:column] - 1},
94+
end: %Position{line: range.end[:line] - 1, character: range.end[:column] - 1}
95+
}
96+
end
97+
98+
defp matches_debug?({:|>, ctx, [_, {{:., ctx, [{:__aliases__, _, [:IO]}, f]}, _, _}]}, pos), do: pos[:line] == ctx[:line]
99+
defp matches_debug?({:dbg, ctx, []}, pos), do: pos[:line] == ctx[:line]
100+
defp matches_debug?({{:., ctx, [{:__aliases__, _, [:IO]}, f]}, _, _}, pos) when f in [:puts, :inspect], do: pos[:line] == ctx[:line]
101+
defp matches_debug?({{:., ctx, [{:__aliases__, _, [:IEx]}, :pry]}, _, _}, pos), do: pos[:line] == ctx[:line]
102+
defp matches_debug?({{:., ctx, [{:__aliases__, _, [:Mix]}, :env]}, _, _}, pos), do: pos[:line] == ctx[:line]
103+
defp matches_debug?(_, _), do: false
33104
end

test/next_ls/extensions/credo_extension/remove_debugger_test.exs

Lines changed: 144 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,30 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
1313
String.split(
1414
"""
1515
defmodule Test.Debug do
16-
def hello() do
17-
IO.inspect("foo")
16+
def hello(arg) do
17+
IO.inspect(arg, label: "DEBUG")
18+
arg
1819
end
1920
end
2021
""",
2122
"\n"
2223
)
2324

24-
start = %Position{character: 0, line: 2}
25+
expected_edit =
26+
String.trim("""
27+
defmodule Test.Debug do
28+
def hello(arg) do
29+
arg
30+
end
31+
end
32+
""")
33+
34+
35+
start = %Position{character: 4, line: 2}
2536

2637
diagnostic = %GenLSP.Structures.Diagnostic{
27-
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.Dbg"},
28-
message: "you must require Logger before invoking the macro Logger.info/1",
38+
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.IoInspect"},
39+
message: "There should be no calls to `IO.inspect/2`",
2940
source: "Elixir",
3041
range: %GenLSP.Structures.Range{
3142
start: start,
@@ -44,8 +55,8 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
4455
changes: %{
4556
^uri => [
4657
%TextEdit{
47-
new_text: "",
48-
range: %Range{start: ^start, end: %{line: 3, character: 0}}
58+
new_text: expected_edit,
59+
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
4960
}
5061
]
5162
}
@@ -66,14 +77,24 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
6677
String.split(
6778
"""
6879
defmodule Test.Debug do
69-
def hello() do
80+
def hello(arg) do
7081
#{code}
82+
arg
7183
end
7284
end
7385
""",
7486
"\n"
7587
)
7688

89+
expected_edit =
90+
String.trim("""
91+
defmodule Test.Debug do
92+
def hello(arg) do
93+
arg
94+
end
95+
end
96+
""")
97+
7798
start = %Position{character: 4, line: 2}
7899

79100
diagnostic = %GenLSP.Structures.Diagnostic{
@@ -97,12 +118,125 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
97118
changes: %{
98119
^uri => [
99120
%TextEdit{
100-
new_text: "",
101-
range: %Range{start: %{line: 2, character: 0}, end: %{line: 3, character: 0}}
121+
new_text: ^expected_edit,
122+
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
102123
}
103124
]
104125
}
105126
} = code_action.edit
106127
end
107128
end
129+
130+
test "works on multiple expressions on one line" do
131+
text =
132+
String.split(
133+
"""
134+
defmodule Test.Debug do
135+
def hello(arg) do
136+
IO.inspect(arg, label: "DEBUG"); arg
137+
end
138+
end
139+
""",
140+
"\n"
141+
)
142+
143+
expected_edit =
144+
String.trim("""
145+
defmodule Test.Debug do
146+
def hello(arg) do
147+
arg
148+
end
149+
end
150+
""")
151+
152+
153+
start = %Position{character: 4, line: 2}
154+
155+
diagnostic = %GenLSP.Structures.Diagnostic{
156+
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.IoInspect"},
157+
message: "There should be no calls to `IO.inspect/2`",
158+
source: "Elixir",
159+
range: %GenLSP.Structures.Range{
160+
start: start,
161+
end: %{start | character: 999}
162+
}
163+
}
164+
165+
uri = "file:///home/owner/my_project/hello.ex"
166+
167+
assert [code_action] = RemoveDebugger.new(diagnostic, text, uri)
168+
assert is_struct(code_action, CodeAction)
169+
assert [diagnostic] == code_action.diagnostics
170+
assert code_action.title == "Remove debugger"
171+
172+
assert %WorkspaceEdit{
173+
changes: %{
174+
^uri => [
175+
%TextEdit{
176+
new_text: ^expected_edit,
177+
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
178+
}
179+
]
180+
}
181+
} = code_action.edit
182+
end
183+
184+
test "handles pipe calls" do
185+
text =
186+
String.split(
187+
"""
188+
defmodule Test.Debug do
189+
def hello(arg) do
190+
arg
191+
|> Enum.map(& &1 * &1)
192+
|> IO.inspect(label: "FOO")
193+
|> Enum.sum()
194+
end
195+
end
196+
""",
197+
"\n"
198+
)
199+
200+
expected_edit =
201+
String.trim("""
202+
defmodule Test.Debug do
203+
def hello(arg) do
204+
arg
205+
|> Enum.map(& &1 * &1)
206+
|> Enum.sum()
207+
end
208+
end
209+
""")
210+
211+
212+
start = %Position{character: 10, line: 4}
213+
214+
diagnostic = %GenLSP.Structures.Diagnostic{
215+
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.IoInspect"},
216+
message: "There should be no calls to `IO.inspect/2`",
217+
source: "Elixir",
218+
range: %GenLSP.Structures.Range{
219+
start: start,
220+
end: %{start | character: 999}
221+
}
222+
}
223+
224+
uri = "file:///home/owner/my_project/hello.ex"
225+
226+
assert [code_action] = RemoveDebugger.new(diagnostic, text, uri)
227+
assert is_struct(code_action, CodeAction)
228+
assert [diagnostic] == code_action.diagnostics
229+
assert code_action.title == "Remove debugger"
230+
231+
assert %WorkspaceEdit{
232+
changes: %{
233+
^uri => [
234+
%TextEdit{
235+
new_text: ^expected_edit,
236+
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
237+
}
238+
]
239+
}
240+
} = code_action.edit
241+
end
108242
end

0 commit comments

Comments
 (0)