Skip to content

Commit

Permalink
Use built-in function to convert source code to ast
Browse files Browse the repository at this point in the history
  • Loading branch information
sheldak committed Mar 12, 2024
1 parent a6f5d8a commit 069cf41
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 394 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
defmodule ElixirLS.LanguageServer.Providers.CodeAction.Helpers do
alias ElixirLS.LanguageServer.Protocol.TextEdit
alias ElixirLS.LanguageServer.Providers.CodeMod.Ast
alias ElixirLS.LanguageServer.Providers.CodeMod.Text
alias ElixirLS.LanguageServer.Providers.CodeMod.Diff

@spec to_text_edits(String.t(), String.t()) :: {:ok, [TextEdit.t()]} | :error
def to_text_edits(unformatted_text, updated_text) do
formatted_text =
unformatted_text
|> Code.format_string!(line_length: :infinity)
|> IO.iodata_to_binary()

change_text_edits = Diff.diff(formatted_text, updated_text)

with {:ok, changed_line} <- changed_line(change_text_edits) do
is_line_formatted =
unformatted_text
|> Diff.diff(formatted_text)
|> Enum.filter(fn %TextEdit{range: range} ->
range["start"]["line"] == changed_line or range["end"]["line"] == changed_line
end)
|> Enum.empty?()

if is_line_formatted do
{:ok, change_text_edits}
else
:error
end
end
end

defp changed_line(text_edits) do
lines =
text_edits
|> Enum.flat_map(fn %TextEdit{range: range} ->
[range["start"]["line"], range["end"]["line"]]
end)
|> Enum.uniq()

case lines do
[line] -> {:ok, line}
_ -> :error
end
end

@spec update_line(TextEdit.t(), non_neg_integer()) :: TextEdit.t()
def update_line(
Expand All @@ -16,38 +55,4 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.Helpers do
}
}
end

@spec to_one_line_string(Ast.t()) :: {:ok, String.t()} | :error
def to_one_line_string(updated_ast) do
updated_ast
|> Ast.to_string()
# We're dealing with a single error on a single line.
# If the line doesn't compile (like it has a do with no end), ElixirSense
# adds additional lines to documents with errors. Also, in case of a one-line do,
# ElixirSense creates do with end from the AST.
|> maybe_recover_one_line_do(updated_ast)
|> Text.fetch_line(0)
end

@do_regex ~r/\s*do\s*/
defp maybe_recover_one_line_do(updated_text, {_name, context, _children} = _updated_ast) do
wrong_do_end_conditions = [
not Keyword.has_key?(context, :do),
not Keyword.has_key?(context, :end),
Regex.match?(@do_regex, updated_text),
String.ends_with?(updated_text, "\nend")
]

if Enum.all?(wrong_do_end_conditions) do
updated_text
|> String.replace(@do_regex, ", do: ")
|> String.trim_trailing("\nend")
else
updated_text
end
end

defp maybe_recover_one_line_do(updated_text, _updated_ast) do
updated_text
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do

use ElixirLS.LanguageServer.Protocol

alias ElixirLS.LanguageServer.Protocol.TextEdit
alias ElixirLS.LanguageServer.Providers.CodeAction.CodeActionResult
alias ElixirLS.LanguageServer.Providers.CodeMod.Ast
alias ElixirLS.LanguageServer.Providers.CodeMod.Diff
alias ElixirLS.LanguageServer.Providers.CodeMod.Text
alias ElixirLS.LanguageServer.SourceFile
alias ElixirSense.Core.Parser

Expand All @@ -18,11 +17,14 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
@spec apply(SourceFile.t(), String.t(), [map()]) :: [CodeActionResult.t()]
def apply(%SourceFile{} = source_file, uri, diagnostics) do
Enum.flat_map(diagnostics, fn diagnostic ->
with {:ok, module, function, arity, line_number} <- extract_function_and_line(diagnostic),
{:ok, suggestions} <- prepare_suggestions(module, function, arity) do
to_code_actions(source_file, line_number, module, function, suggestions, uri)
else
_ -> []
case extract_function_and_line(diagnostic) do
{:ok, module, function, arity, line} ->
suggestions = prepare_suggestions(module, function, arity)

build_code_actions(source_file, line, module, function, suggestions, uri)

:error ->
[]
end
end)
end
Expand All @@ -38,6 +40,9 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
with [[_, module_and_function, arity]] <- Regex.scan(@function_re, message),
{:ok, module, function_name} <- separate_module_from_function(module_and_function) do
{:ok, module, function_name, String.to_integer(arity)}
else
_ ->
:error
end
end

Expand Down Expand Up @@ -65,17 +70,14 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
@function_threshold 0.77
@max_suggestions 5
defp prepare_suggestions(module, function, arity) do
suggestions =
for {module_function, ^arity} <- module_functions(module),
distance = module_function |> Atom.to_string() |> String.jaro_distance(function),
distance >= @function_threshold do
{distance, module_function}
end
|> Enum.sort(:desc)
|> Enum.take(@max_suggestions)
|> Enum.map(fn {_distance, module_function} -> module_function end)

{:ok, suggestions}
for {module_function, ^arity} <- module_functions(module),
distance = module_function |> Atom.to_string() |> String.jaro_distance(function),
distance >= @function_threshold do
{distance, module_function}
end
|> Enum.sort(:desc)
|> Enum.take(@max_suggestions)
|> Enum.map(fn {_distance, module_function} -> module_function end)
end

defp module_functions(module) do
Expand All @@ -86,12 +88,12 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
end
end

defp to_code_actions(%SourceFile{} = source_file, line_number, module, name, suggestions, uri) do
defp build_code_actions(%SourceFile{} = source_file, line, module, name, suggestions, uri) do
suggestions
|> Enum.reduce([], fn suggestion, acc ->
case apply_transform(source_file, line_number, module, name, suggestion) do
case text_edits(source_file, line, module, name, suggestion) do
{:ok, [_ | _] = text_edits} ->
text_edits = Enum.map(text_edits, &update_line(&1, line_number))
text_edits = Enum.map(text_edits, &update_line(&1, line))

code_action =
CodeActionResult.new("Rename to #{suggestion}", "quickfix", text_edits, uri)
Expand All @@ -105,58 +107,51 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
|> Enum.reverse()
end

defp apply_transform(source_file, line_number, module, name, suggestion) do
with {:ok, text} <- fetch_line(source_file, line_number),
{:ok, ast} <- Ast.from(text) do
function_atom = String.to_atom(name)

leading_indent = Text.leading_indent(text)
trailing_comment = Text.trailing_comment(text)

ast
|> Macro.postwalk(fn
{:., function_meta, [{:__aliases__, module_meta, module_alias}, ^function_atom]} ->
case expand_alias(source_file, module_alias, line_number) do
{:ok, ^module} ->
{:., function_meta, [{:__aliases__, module_meta, module_alias}, suggestion]}

_ ->
{:., function_meta, [{:__aliases__, module_meta, module_alias}, function_atom]}
end

# erlang call
{:., function_meta, [^module, ^function_atom]} ->
{:., function_meta, [module, suggestion]}

other ->
other
end)
|> to_one_line_string()
|> case do
{:ok, updated_text} ->
text_edits = Diff.diff(text, "#{leading_indent}#{updated_text}#{trailing_comment}")

{:ok, text_edits}

:error ->
:error
end
@spec text_edits(SourceFile.t(), non_neg_integer(), atom(), String.t(), atom()) ::
{:ok, [TextEdit.t()]} | :error
defp text_edits(%SourceFile{} = source_file, line, module, name, suggestion) do
with {:ok, updated_text} <- apply_transform(source_file, line, module, name, suggestion) do
to_text_edits(source_file.text, updated_text)
end
end

defp fetch_line(%SourceFile{} = source_file, line_number) do
lines = SourceFile.lines(source_file)
defp apply_transform(source_file, line, module, name, suggestion) do
with {:ok, ast, comments} <- Ast.from(source_file) do
function_atom = String.to_atom(name)

if length(lines) > line_number do
{:ok, Enum.at(lines, line_number)}
else
:error
one_based_line = line + 1

updated_text =
ast
|> Macro.postwalk(fn
{:., [line: ^one_based_line],
[{:__aliases__, module_meta, module_alias}, ^function_atom]} ->
case expand_alias(source_file, module_alias, line) do
{:ok, ^module} ->
{:., [line: one_based_line],
[{:__aliases__, module_meta, module_alias}, suggestion]}

_ ->
{:., [line: one_based_line],
[{:__aliases__, module_meta, module_alias}, function_atom]}
end

# erlang call
{:., [line: ^one_based_line], [{:__block__, module_meta, [^module]}, ^function_atom]} ->
{:., [line: one_based_line], [{:__block__, module_meta, [module]}, suggestion]}

other ->
other
end)
|> Ast.to_string(comments)

{:ok, updated_text}
end
end

@spec expand_alias(SourceFile.t(), [atom()], non_neg_integer()) :: {:ok, atom()} | :error
defp expand_alias(source_file, module_alias, line_number) do
with {:ok, aliases} <- aliases_at(source_file, line_number) do
defp expand_alias(source_file, module_alias, line) do
with {:ok, aliases} <- aliases_at(source_file, line) do
aliases
|> Enum.map(fn {module, aliased} ->
module = module |> module_to_alias() |> List.first()
Expand All @@ -177,8 +172,8 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction do
end
end

defp aliases_at(source_file, line_number) do
one_based_line = line_number + 1
defp aliases_at(source_file, line) do
one_based_line = line + 1

metadata = Parser.parse_string(source_file.text, true, true, {one_based_line, 1})

Expand Down
Loading

0 comments on commit 069cf41

Please sign in to comment.