Skip to content

Commit

Permalink
Refactor to use the AST
Browse files Browse the repository at this point in the history
  • Loading branch information
NJichev committed Apr 22, 2024
1 parent 88f5bca commit 4135f6f
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 29 deletions.
109 changes: 90 additions & 19 deletions lib/next_ls/extensions/credo_extension/code_action/remove_debugger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,103 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebugger do
@moduledoc false

alias GenLSP.Structures.CodeAction
alias GenLSP.Structures.Position
alias GenLSP.Structures.Diagnostic
alias GenLSP.Structures.Range
alias GenLSP.Structures.TextEdit
alias GenLSP.Structures.WorkspaceEdit
alias NextLS.ASTHelpers
alias NextLS.EditHelpers
alias Sourceror.Zipper, as: Z
@line_length 121

def new(diagnostic, _text, uri) do
%Diagnostic{range: %Range{start: start}} = diagnostic

[
%CodeAction{
title: "Remove debugger",
diagnostics: [diagnostic],
edit: %WorkspaceEdit{
changes: %{
uri => [
%TextEdit{
new_text: "",
range: %Range{
start: %{start | character: 0},
end: %{start | character: 0, line: start.line + 1}
def new(diagnostic, text, uri) do
%Diagnostic{range: range} = diagnostic

with {:ok, ast, comments} <- parse(text),
{:ok, defm} <- ASTHelpers.get_surrounding_module(ast, range.start) do
range = make_range(defm)
indent = EditHelpers.get_indent(text, range.start.line)
diagnostic.range.start
ast_without_debugger = remove_debugger(defm, diagnostic.range.start)

comments =
Enum.filter(comments, fn comment ->
comment.line > range.start.line && comment.line <= range.end.line
end)

to_algebra_opts = [comments: comments]
doc = Code.quoted_to_algebra(ast_without_debugger, to_algebra_opts)
formatted = doc |> Inspect.Algebra.format(@line_length) |> IO.iodata_to_binary()

[
%CodeAction{
title: "Remove debugger",
diagnostics: [diagnostic],
edit: %WorkspaceEdit{
changes: %{
uri => [
%TextEdit{
new_text: EditHelpers.add_indent_to_edit(formatted, indent),
range: range
}
}
]
]
}
}
}
}
]
]
else
{:error, message} ->
%GenLSP.ErrorResponse{code: ErrorCodes.parse_error(), message: inspect(message)}

Check warning on line 52 in lib/next_ls/extensions/credo_extension/code_action/remove_debugger.ex

View workflow job for this annotation

GitHub Actions / dialyzer

unknown_function

Function ErrorCodes.parse_error/0 does not exist.
end
end

defp remove_debugger(ast, position) do
pos = [line: position.line + 1, column: position.character + 1]
result =
ast
|> Z.zip()
|> Z.traverse(fn tree ->
node = Z.node(tree)
range = Sourceror.get_range(node)

if matches_debug?(node, pos) &&
Sourceror.compare_positions(range.start, pos) in [:lt, :eq] &&
Sourceror.compare_positions(range.end, pos) in [:gt, :eq] do
Z.remove(tree)
else
tree
end
end)
|> Z.node()
end

defp parse(lines) do
lines
|> Enum.join("\n")
|> Spitfire.parse_with_comments(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
|> case do
{:error, ast, comments, _errors} ->
{:ok, ast, comments}

other ->
other
end
end

defp make_range(original_ast) do
range = Sourceror.get_range(original_ast)

%Range{
start: %Position{line: range.start[:line] - 1, character: range.start[:column] - 1},
end: %Position{line: range.end[:line] - 1, character: range.end[:column] - 1}
}
end

defp matches_debug?({:|>, ctx, [_, {{:., ctx, [{:__aliases__, _, [:IO]}, f]}, _, _}]}, pos), do: pos[:line] == ctx[:line]
defp matches_debug?({:dbg, ctx, []}, pos), do: pos[:line] == ctx[:line]
defp matches_debug?({{:., ctx, [{:__aliases__, _, [:IO]}, f]}, _, _}, pos) when f in [:puts, :inspect], do: pos[:line] == ctx[:line]
defp matches_debug?({{:., ctx, [{:__aliases__, _, [:IEx]}, :pry]}, _, _}, pos), do: pos[:line] == ctx[:line]
defp matches_debug?({{:., ctx, [{:__aliases__, _, [:Mix]}, :env]}, _, _}, pos), do: pos[:line] == ctx[:line]
defp matches_debug?(_, _), do: false
end
154 changes: 144 additions & 10 deletions test/next_ls/extensions/credo_extension/remove_debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,30 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
String.split(
"""
defmodule Test.Debug do
def hello() do
IO.inspect("foo")
def hello(arg) do
IO.inspect(arg, label: "DEBUG")
arg
end
end
""",
"\n"
)

start = %Position{character: 0, line: 2}
expected_edit =
String.trim("""
defmodule Test.Debug do
def hello(arg) do
arg
end
end
""")


start = %Position{character: 4, line: 2}

diagnostic = %GenLSP.Structures.Diagnostic{
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.Dbg"},
message: "you must require Logger before invoking the macro Logger.info/1",
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.IoInspect"},
message: "There should be no calls to `IO.inspect/2`",
source: "Elixir",
range: %GenLSP.Structures.Range{
start: start,
Expand All @@ -44,8 +55,8 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
changes: %{
^uri => [
%TextEdit{
new_text: "",
range: %Range{start: ^start, end: %{line: 3, character: 0}}
new_text: expected_edit,
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
}
]
}
Expand All @@ -66,14 +77,24 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
String.split(
"""
defmodule Test.Debug do
def hello() do
def hello(arg) do
#{code}
arg
end
end
""",
"\n"
)

expected_edit =
String.trim("""
defmodule Test.Debug do
def hello(arg) do
arg
end
end
""")

start = %Position{character: 4, line: 2}

diagnostic = %GenLSP.Structures.Diagnostic{
Expand All @@ -97,12 +118,125 @@ defmodule NextLS.CredoExtension.CodeAction.RemoveDebuggerTest do
changes: %{
^uri => [
%TextEdit{
new_text: "",
range: %Range{start: %{line: 2, character: 0}, end: %{line: 3, character: 0}}
new_text: ^expected_edit,
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
}
]
}
} = code_action.edit
end
end

test "works on multiple expressions on one line" do
text =
String.split(
"""
defmodule Test.Debug do
def hello(arg) do
IO.inspect(arg, label: "DEBUG"); arg
end
end
""",
"\n"
)

expected_edit =
String.trim("""
defmodule Test.Debug do
def hello(arg) do
arg
end
end
""")


start = %Position{character: 4, line: 2}

diagnostic = %GenLSP.Structures.Diagnostic{
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.IoInspect"},
message: "There should be no calls to `IO.inspect/2`",
source: "Elixir",
range: %GenLSP.Structures.Range{
start: start,
end: %{start | character: 999}
}
}

uri = "file:///home/owner/my_project/hello.ex"

assert [code_action] = RemoveDebugger.new(diagnostic, text, uri)
assert is_struct(code_action, CodeAction)
assert [diagnostic] == code_action.diagnostics
assert code_action.title == "Remove debugger"

assert %WorkspaceEdit{
changes: %{
^uri => [
%TextEdit{
new_text: ^expected_edit,
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
}
]
}
} = code_action.edit
end

test "handles pipe calls" do
text =
String.split(
"""
defmodule Test.Debug do
def hello(arg) do
arg
|> Enum.map(& &1 * &1)
|> IO.inspect(label: "FOO")
|> Enum.sum()
end
end
""",
"\n"
)

expected_edit =
String.trim("""
defmodule Test.Debug do
def hello(arg) do
arg
|> Enum.map(& &1 * &1)
|> Enum.sum()
end
end
""")


start = %Position{character: 10, line: 4}

diagnostic = %GenLSP.Structures.Diagnostic{
data: %{"namespace" => "credo", "check" => "Elixir.Credo.Check.Warning.IoInspect"},
message: "There should be no calls to `IO.inspect/2`",
source: "Elixir",
range: %GenLSP.Structures.Range{
start: start,
end: %{start | character: 999}
}
}

uri = "file:///home/owner/my_project/hello.ex"

assert [code_action] = RemoveDebugger.new(diagnostic, text, uri)
assert is_struct(code_action, CodeAction)
assert [diagnostic] == code_action.diagnostics
assert code_action.title == "Remove debugger"

assert %WorkspaceEdit{
changes: %{
^uri => [
%TextEdit{
new_text: ^expected_edit,
range: %Range{start: %{line: 0, character: 0}, end: %{line: 5, character: 3}}
}
]
}
} = code_action.edit
end
end

0 comments on commit 4135f6f

Please sign in to comment.