From 10978db80f0ce559a9ffbd33cddc762872ef4bc5 Mon Sep 17 00:00:00 2001 From: Sergei Maximov Date: Sun, 27 Aug 2023 00:28:23 +0300 Subject: [PATCH 1/2] Allow to lookup Elixir source files under configured location --- lib/elixir_sense/location.ex | 57 ++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/lib/elixir_sense/location.ex b/lib/elixir_sense/location.ex index 3140b385..dcd5ef6e 100644 --- a/lib/elixir_sense/location.ex +++ b/lib/elixir_sense/location.ex @@ -18,13 +18,11 @@ defmodule ElixirSense.Location do } defstruct [:type, :file, :line, :column] - defguardp file_exists(file) when file not in ["non_existing", nil, ""] - @spec find_mod_fun_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) :: Location.t() | nil def find_mod_fun_source(mod, fun, arity) do case find_mod_file(mod) do - {mod, file} when file_exists(file) -> + file when is_binary(file) -> find_fun_position({mod, file}, fun, arity) _ -> @@ -34,10 +32,10 @@ defmodule ElixirSense.Location do @spec find_type_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) :: Location.t() | nil - def find_type_source(mod, fun, arity) do + def find_type_source(mod, type, arity) do case find_mod_file(mod) do - {mod, file} when file_exists(file) -> - find_type_position({mod, file}, fun, arity) + file when is_binary(file) -> + find_type_position({mod, file}, type, arity) _ -> nil @@ -47,6 +45,10 @@ defmodule ElixirSense.Location do defp find_mod_file(Elixir), do: nil defp find_mod_file(module) do + find_elixir_file(module) || find_erlang_file(module) + end + + defp find_elixir_file(module) do file = if Code.ensure_loaded?(module) do case module.module_info(:compile)[:source] do @@ -55,26 +57,45 @@ defmodule ElixirSense.Location do end end - file = - if file && File.exists?(file, [:raw]) do + if file do + if File.exists?(file, [:raw]) do file else - with {_module, _binary, beam_filename} <- :code.get_object_code(module), - erl_file = - beam_filename - |> to_string - |> String.replace( - ~r/(.+)\/ebin\/([^\s]+)\.beam$/, - "\\1/src/\\2.erl" + # If Elixir was built in a sandboxed environment, + # `module.module_info(:compile)[:source]` would point to a non-existing + # location; in this case try to find a "core" Elixir source file under + # the configured Elixir source path. + with elixir_src when is_binary(elixir_src) <- + Application.get_env(:elixir_sense, :elixir_src), + file = + String.replace( + file, + Regex.recompile!(~r<^(?:.+)(/lib/.+\.ex)$>U), + elixir_src <> "\\1" ), - true <- File.exists?(erl_file, [:raw]) do - erl_file + true <- File.exists?(file, [:raw]) do + file else _ -> nil end end + end + end - {module, file} + defp find_erlang_file(module) do + with {_module, _binary, beam_filename} <- :code.get_object_code(module), + erl_file = + beam_filename + |> to_string + |> String.replace( + Regex.recompile!(~r/(.+)\/ebin\/([^\s]+)\.beam$/), + "\\1/src/\\2.erl" + ), + true <- File.exists?(erl_file, [:raw]) do + erl_file + else + _ -> nil + end end defp find_fun_position({mod, file}, fun, arity) do From 372ab294ddb1ef8c681c25834c78366387b30492 Mon Sep 17 00:00:00 2001 From: Sergei Maximov Date: Tue, 29 Aug 2023 00:36:34 +0300 Subject: [PATCH 2/2] First attempt on Elixir location tests --- test/elixir_sense/location_test.exs | 32 +++++++++++++++++ .../mock_elixir_src/lib/elixir/lib/string.ex | 35 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 test/elixir_sense/location_test.exs create mode 100644 test/misc/mock_elixir_src/lib/elixir/lib/string.ex diff --git a/test/elixir_sense/location_test.exs b/test/elixir_sense/location_test.exs new file mode 100644 index 00000000..035d6741 --- /dev/null +++ b/test/elixir_sense/location_test.exs @@ -0,0 +1,32 @@ +defmodule ElixirSense.LocationTest do + use ExUnit.Case, async: false + + import ElixirSense.Location + + setup do + elixir_src = Path.join(File.cwd!(), "/test/misc/mock_elixir_src") + Application.put_env(:elixir_sense, :elixir_src, elixir_src) + + on_exit(fn -> + Application.delete_env(:elixir_sense, :elixir_src) + end) + end + + describe "find_mod_fun_source/3" do + test "returns location of a core Elixir function" do + assert %ElixirSense.Location{type: :function, line: 26, column: 3, file: file} = + find_mod_fun_source(String, :length, 1) + + assert String.ends_with?(file, "/mock_elixir_src/lib/elixir/lib/string.ex") + end + end + + describe "find_type_source/3" do + test "returns location of a core Elixir type" do + assert %ElixirSense.Location{type: :typespec, line: 11, column: 3, file: file} = + find_type_source(String, :t, 0) + + assert String.ends_with?(file, "/mock_elixir_src/lib/elixir/lib/string.ex") + end + end +end diff --git a/test/misc/mock_elixir_src/lib/elixir/lib/string.ex b/test/misc/mock_elixir_src/lib/elixir/lib/string.ex new file mode 100644 index 00000000..221ff5b1 --- /dev/null +++ b/test/misc/mock_elixir_src/lib/elixir/lib/string.ex @@ -0,0 +1,35 @@ +import Kernel, except: [length: 1] + +defmodule String do + @typedoc """ + A UTF-8 encoded binary. + + The types `String.t()` and `binary()` are equivalent to analysis tools. + Although, for those reading the documentation, `String.t()` implies + it is a UTF-8 encoded binary. + """ + @type t :: binary + + @doc """ + Returns the number of Unicode graphemes in a UTF-8 string. + + ## Examples + + iex> String.length("elixir") + 6 + + iex> String.length("եոգլի") + 5 + + """ + @spec length(t) :: non_neg_integer + def length(string) when is_binary(string), do: length(string, 0) + + defp length(gcs, acc) do + case :unicode_util.gc(gcs) do + [_ | rest] -> length(rest, acc + 1) + [] -> acc + {:error, <<_, rest::bits>>} -> length(rest, acc + 1) + end + end +end