Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide next best definition when no exact match is present. #807

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Definition do
alias Lexical.Document.Location
alias Lexical.Document.Position
alias Lexical.Formats
alias Lexical.RemoteControl.Analyzer
alias Lexical.RemoteControl.CodeIntelligence.Entity
alias Lexical.RemoteControl.Search.Indexer.Entry
alias Lexical.RemoteControl.Search.Store
Expand All @@ -22,76 +23,127 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Definition do
end
end

defp fetch_definition({type, entity} = resolved, %Analysis{} = analysis, %Position{} = position)
defp fetch_definition({type, _entity} = resolved, %Analysis{} = analysis, %Position{} = pos)
when type in [:struct, :module] do
with {:ok, nil} <- exact_module_definition(resolved) do
elixir_sense_definition(analysis, pos)
end
end

defp fetch_definition({:call, _m, _f, _a} = resolved, %Analysis{} = nlss, %Position{} = pos) do
with {:ok, nil} <- exact_call_definition(resolved, nlss, pos),
{:ok, nil} <- elixir_sense_definition(nlss, pos) do
nearest_arity_call_definition(resolved, nlss, pos)
end
end

defp fetch_definition(_, %Analysis{} = analysis, %Position{} = position) do
elixir_sense_definition(analysis, position)
end

defp exact_module_definition({type, entity} = resolved) do
module = Formats.module(entity)

locations =
case Store.exact(module, type: type, subtype: :definition) do
{:ok, entries} ->
for entry <- entries,
result = to_location(entry),
match?({:ok, _}, result) do
{:ok, location} = result
location
end

_ ->
[]
end

maybe_fallback_to_elixir_sense(resolved, locations, analysis, position)
end

defp fetch_definition(
{:call, module, function, arity} = resolved,
%Analysis{} = analysis,
%Position{} = position
) do
mfa = Formats.mfa(module, function, arity)
module
|> query_search_index_exact(type: type, subtype: :definition)
|> entries_to_locations()

definitions =
mfa
|> query_search_index(subtype: :definition)
|> Stream.flat_map(fn entry ->
case entry do
%Entry{type: {:function, :delegate}} ->
mfa = get_in(entry, [:metadata, :original_mfa])
query_search_index(mfa, subtype: :definition) ++ [entry]

_ ->
[entry]
end
end)
|> Stream.uniq_by(& &1.subject)
case locations do
[location] ->
{:ok, location}

locations =
for entry <- definitions,
result = to_location(entry),
match?({:ok, _}, result) do
{:ok, location} = result
location
end
[_ | _] ->
{:ok, locations}

maybe_fallback_to_elixir_sense(resolved, locations, analysis, position)
[] ->
Logger.info("No definition found for #{inspect(resolved)} with Indexer.")
{:ok, nil}
end
end

defp fetch_definition(_, %Analysis{} = analysis, %Position{} = position) do
elixir_sense_definition(analysis, position)
end
defp exact_call_definition({:call, module, function, arity} = resolved, analysis, position) do
mfa = Formats.mfa(module, function, arity)

locations =
mfa
|> query_search_index_exact(subtype: :definition)
|> Stream.flat_map(&resolve_defdelegate/1)
|> Stream.uniq_by(& &1.subject)
|> maybe_reject_private_defs(module, analysis, position)
|> entries_to_locations()

defp maybe_fallback_to_elixir_sense(resolved, locations, analysis, position) do
case locations do
[location] ->
{:ok, location}

[_ | _] ->
{:ok, locations}

[] ->
Logger.info("No definition found for #{inspect(resolved)} with Indexer.")
{:ok, nil}
end
end

elixir_sense_definition(analysis, position)
defp nearest_arity_call_definition({:call, m, f, _a} = resolved, nlss, pos) do
mf_prefix = Formats.mf(m, f)

locations =
mf_prefix
|> query_search_index_prefix(subtype: :definition)
|> Stream.flat_map(&resolve_defdelegate/1)
|> Stream.uniq_by(& &1.subject)
|> maybe_reject_private_defs(m, nlss, pos)
# sort by arity and take the lowest.
|> Enum.sort(fn %Entry{} = a, %Entry{} = b ->
String.last(a.subject) < String.last(b.subject)
end)
|> Enum.take(1)
|> entries_to_locations()

case locations do
[location] ->
{:ok, location}

_ ->
[_ | _] ->
{:ok, locations}

[] ->
Logger.info("No nearest-arity definition found for #{inspect(resolved)} with Indexer.")
{:ok, nil}
end
end

def resolve_defdelegate(%Entry{type: {:function, :delegate}} = entry) do
mfa = get_in(entry, [:metadata, :original_mfa])
query_search_index_exact(mfa, subtype: :definition) ++ [entry]
end

def resolve_defdelegate(entry) do
[entry]
end

defp maybe_reject_private_defs(entries, module, analysis, position) do
case Analyzer.current_module(analysis, position) do
{:ok, module_at_position} ->
if module != module_at_position do
Stream.reject(entries, &(&1.type == {:function, :private}))
else
entries
end

:error ->
entries
end
end

defp entries_to_locations(entries) do
for entry <- entries,
result = to_location(entry),
match?({:ok, _}, result) do
{:ok, location} = result
location
end
end

Expand Down Expand Up @@ -171,8 +223,18 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Definition do
end
end

defp query_search_index(subject, condition) do
case Store.exact(subject, condition) do
defp query_search_index_exact(subject, constraints) do
case Store.exact(subject, constraints) do
{:ok, entries} ->
entries

_ ->
[]
end
end

defp query_search_index_prefix(subject, constraints) do
case Store.prefix(subject, constraints) do
{:ok, entries} ->
entries

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ defmodule MyDefinition do
"Hello, #{name}!"
end

def greet(name, name2) do
"Hello, #{name} and #{name2}!"
end

defmacro print_hello do
quote do
IO.puts("Hello, world!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,27 @@ defmodule Lexical.RemoteControl.CodeIntelligence.DefinitionTest do
end
end

describe "definition/2 when no exact is available" do
setup [:with_referenced_file]

test "find the definition of a remote function call", %{project: project, uri: referenced_uri} do
subject_module = ~q[
defmodule UsesRemoteFunction do
alias MyDefinition

def uses_greet() do
MyDefinition.gree|t("World", "Bad", "Arity")
end
end
]

assert {:ok, ^referenced_uri, definition_line} =
definition(project, subject_module, referenced_uri)

assert definition_line == ~S[ def «greet(name)» do]
end
end

describe "edge cases" do
setup [:with_referenced_file]

Expand Down
29 changes: 29 additions & 0 deletions projects/lexical_shared/lib/lexical/formats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ defmodule Lexical.Formats do
millis
end

@spec plural(integer(), String.t(), String.t()) :: String.t()
def plural(count, singular, plural) do
case count do
0 -> templatize(count, plural)
Expand All @@ -98,10 +99,38 @@ defmodule Lexical.Formats do
end
end

@doc """
Formats a module, function, and arity into a string.

## Examples

iex> alias LXical.Formats
LXical.Formats
iex> mfa(Formats, :mfa, 3)
"LXical.Formats.mfa/3"
iex> mfa("Formats", "mfa", 3)
"LXical.Formats.mfa/3"

"""
@spec mfa(atom() | binary(), any(), any()) :: String.t()
def mfa(module, function, arity) do
"#{module(module)}.#{function}/#{arity}"
end

@doc """
Formats a module and function without arity.

## Examples

iex> mf(LXical.Formats, mf)
"LXical.Formats.mf/"

"""
@spec mf(atom() | binary(), any()) :: String.t()
def mf(module, function) do
"#{module(module)}.#{function}/"
end

defp templatize(count, template) do
count_string = Integer.to_string(count)
String.replace(template, "${count}", count_string)
Expand Down
Loading