diff --git a/VERSION b/VERSION index 5a03fb737..b2b1c9d11 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.20.0 +0.21.0-dev diff --git a/apps/debug_adapter/lib/debug_adapter/server.ex b/apps/debug_adapter/lib/debug_adapter/server.ex index 92e38a040..ed6f97f08 100644 --- a/apps/debug_adapter/lib/debug_adapter/server.ex +++ b/apps/debug_adapter/lib/debug_adapter/server.ex @@ -1315,7 +1315,7 @@ defmodule ElixirLS.DebugAdapter.Server do metadata = %ElixirSense.Core.Metadata{} results = - ElixirSense.Providers.Suggestion.Complete.complete(prefix, env, metadata, {1, 1}) + ElixirLS.Utils.CompletionEngine.complete(prefix, env, metadata, {1, 1}) |> Enum.map(&ElixirLS.DebugAdapter.Completions.map/1) %{"targets" => results} diff --git a/apps/elixir_ls_utils/lib/completion_engine.ex b/apps/elixir_ls_utils/lib/completion_engine.ex new file mode 100644 index 000000000..bc97545df --- /dev/null +++ b/apps/elixir_ls_utils/lib/completion_engine.ex @@ -0,0 +1,1526 @@ +# This file includes modified code extracted from the elixir project. Namely: +# +# https://github.com/elixir-lang/elixir/blob/v1.1/lib/iex/lib/iex/autocomplete.exs +# +# The original code is licensed as follows: +# +# Copyright 2012 Plataformatec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This module is based on IEx.Autocomplete from version ~ 1.1 +# with some changes inspired by Alchemist.Completer (itself based on IEx.Autocomplete). +# Since then the codebases have diverged as the requirements +# put on editor and REPL autocomplete are different. +# However some relevant changes have been merged back +# from upstream Elixir (1.13). +# Changes made to the original version include: +# - different result format with added docs and spec +# - built in and private funcs are not excluded +# - hint generation removed +# - added expansion basing on metadata besides introspection +# - uses custom docs extraction function +# - gets metadata by argument instead of environment variables +# (original Elixir 1.1) and later GenServer +# - no signature completion as it's handled by signature provider +# - added attribute completion +# - improved completion after %, ^ and & operators + +defmodule ElixirLS.Utils.CompletionEngine do + @moduledoc """ + Provides generic completion for functions, macros, attributes, variables + """ + alias ElixirSense.Core.Applications + alias ElixirSense.Core.Behaviours + alias ElixirSense.Core.Binding + alias ElixirSense.Core.BuiltinAttributes + alias ElixirSense.Core.BuiltinFunctions + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Core.Struct + alias ElixirSense.Core.TypeInfo + + alias ElixirLS.Utils.Matcher + require Logger + + @module_results_cache_key :"#{__MODULE__}_module_results_cache" + + @erlang_module_builtin_functions [{:module_info, 0}, {:module_info, 1}] + @elixir_module_builtin_functions [{:__info__, 1}] + @builtin_functions @erlang_module_builtin_functions ++ @elixir_module_builtin_functions + + @type attribute :: %{ + type: :attribute, + name: String.t(), + summary: String.t() | nil + } + + @type variable :: %{ + type: :variable, + name: String.t() + } + + @type func :: %{ + type: :function | :macro, + visibility: :public | :private, + name: String.t(), + needed_require: String.t() | nil, + needed_import: {String.t(), {String.t(), integer()}} | nil, + arity: non_neg_integer, + def_arity: non_neg_integer, + args: String.t(), + args_list: [String.t()], + origin: String.t(), + summary: String.t(), + spec: String.t(), + snippet: String.t() | nil, + metadata: map + } + + @type mod :: %{ + type: :module, + name: String.t(), + subtype: ElixirSense.Core.Introspection.module_subtype(), + summary: String.t(), + metadata: map, + required_alias: String.t() | nil + } + + @type field :: %{ + type: :field, + subtype: :struct_field | :map_key, + name: String.t(), + origin: String.t() | nil, + call?: boolean, + type_spec: String.t() | nil + } + + @type t() :: + mod() + | func() + | variable() + | field() + | attribute() + + @spec complete(String.t(), State.Env.t(), Metadata.t(), {pos_integer, pos_integer}, keyword()) :: + [t()] + def complete(hint, %State.Env{} = env, %Metadata{} = metadata, cursor_position, opts \\ []) do + do_expand(hint |> String.to_charlist(), env, metadata, cursor_position, opts) + end + + def do_expand(code, %State.Env{} = env, %Metadata{} = metadata, cursor_position, opts \\ []) do + # TODO remove when we require elixir 1.13 + only_structs = + case code do + [?% | _] -> true + _ -> false + end + + case NormalizedCode.Fragment.cursor_context(code) do + {:alias, hint} when is_list(hint) -> + expand_aliases(List.to_string(hint), env, metadata, cursor_position, false, opts) + + {:alias, prefix, hint} -> + expand_prefixed_aliases(prefix, hint, env, metadata, cursor_position, false) + + {:unquoted_atom, unquoted_atom} -> + expand_erlang_modules(List.to_string(unquoted_atom), env, metadata) + + {:dot, path, hint} -> + expand_dot( + path, + List.to_string(hint), + false, + env, + metadata, + cursor_position, + only_structs, + opts + ) + + {:dot_arity, path, hint} -> + expand_dot( + path, + List.to_string(hint), + true, + env, + metadata, + cursor_position, + only_structs, + opts + ) + + {:dot_call, _path, _hint} -> + # no need to expand signatures here, we have signatures provider + # IEx calls + # expand_dot_call(path, List.to_atom(hint), env) + # to provide signatures and falls back to expand_local_or_var + expand_expr(env, metadata, cursor_position, opts) + + :expr -> + # IEx calls expand_local_or_var("", env) + # we choose to return more and handle some special cases + # TODO expand_expr(env) after we require elixir 1.13 + case code do + [?^] -> expand_var("", env, metadata) + [?%] -> expand_aliases("", env, metadata, cursor_position, true, opts) + _ -> expand_expr(env, metadata, cursor_position, opts) + end + + {:local_or_var, local_or_var} -> + # TODO consider suggesting struct fields here when we require elixir 1.13 + # expand_struct_fields_or_local_or_var(code, List.to_string(local_or_var), shell) + expand_local_or_var(List.to_string(local_or_var), env, metadata, cursor_position) + + {:local_arity, local} -> + expand_local(List.to_string(local), true, env, metadata, cursor_position) + + {:local_call, _local} -> + # no need to expand signatures here, we have signatures provider + # expand_local_call(List.to_atom(local), env) + # IEx calls + # expand_dot_call(path, List.to_atom(hint), env) + # to provide signatures and falls back to expand_local_or_var + expand_expr(env, metadata, cursor_position, opts) + + # elixir >= 1.13 + {:operator, operator} -> + case operator do + [?^] -> expand_var("", env, metadata) + [?&] -> expand_expr(env, metadata, cursor_position, opts) + _ -> expand_local(List.to_string(operator), false, env, metadata, cursor_position) + end + + # elixir >= 1.13 + {:operator_arity, operator} -> + expand_local(List.to_string(operator), true, env, metadata, cursor_position) + + # elixir >= 1.13 + {:operator_call, _operator} -> + expand_local_or_var("", env, metadata, cursor_position) + + # elixir >= 1.13 + {:sigil, []} -> + expand_sigil(env, metadata, cursor_position) + + # elixir >= 1.13 + {:sigil, [_]} -> + # {:yes, [], ~w|" """ ' ''' \( / < [ { \||c} + # we choose to not provide sigil chars + no() + + # elixir >= 1.13 + {:struct, struct} when is_list(struct) -> + expand_aliases(List.to_string(struct), env, metadata, cursor_position, true, opts) + + # elixir >= 1.14 + {:struct, {:alias, prefix, hint}} -> + expand_prefixed_aliases(prefix, hint, env, metadata, cursor_position, true) + + # elixir >= 1.14 + {:struct, {:dot, path, hint}} -> + expand_dot(path, List.to_string(hint), false, env, metadata, cursor_position, true, opts) + + # elixir >= 1.14 + {:struct, {:module_attribute, attribute}} -> + expand_attribute(List.to_string(attribute), env, metadata) + + # elixir >= 1.14 + {:struct, {:local_or_var, local_or_var}} -> + # TODO consider suggesting struct fields here when we require elixir 1.13 + # expand_struct_fields_or_local_or_var(code, List.to_string(local_or_var), shell) + expand_local_or_var(List.to_string(local_or_var), env, metadata, cursor_position) + + {:module_attribute, attribute} -> + expand_attribute(List.to_string(attribute), env, metadata) + + # elixir >= 1.16 + {:anonymous_call, _} -> + expand_expr(env, metadata, cursor_position, opts) + + :none -> + no() + end + end + + defp expand_dot( + path, + hint, + exact?, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position, + only_structs, + opts + ) do + filter = struct_module_filter(only_structs, env, metadata) + + case expand_dot_path(path, env, metadata) do + {:ok, {:atom, mod}} when hint == "" -> + expand_aliases( + mod, + "", + [], + not only_structs, + env, + metadata, + cursor_position, + filter, + opts + ) + + {:ok, {:atom, mod}} -> + expand_require(mod, hint, exact?, env, metadata, cursor_position) + + {:ok, {:map, fields, _}} -> + expand_map_field_access(fields, hint, :map, env, metadata) + + {:ok, {:struct, fields, type, _}} -> + expand_map_field_access(fields, hint, {:struct, type}, env, metadata) + + _ -> + no() + end + end + + # elixir >= 1.14 + defp expand_dot_path({:var, ~c"__MODULE__"}, %State.Env{} = env, %Metadata{} = _metadata) do + if env.module != nil and Introspection.elixir_module?(env.module) do + {:ok, {:atom, env.module}} + else + :error + end + end + + defp expand_dot_path({:var, var}, %State.Env{} = env, %Metadata{} = metadata) do + value_from_binding({:variable, List.to_atom(var)}, env, metadata) + end + + defp expand_dot_path({:module_attribute, attribute}, %State.Env{} = env, %Metadata{} = metadata) do + value_from_binding({:attribute, List.to_atom(attribute)}, env, metadata) + end + + defp expand_dot_path({:alias, hint}, %State.Env{} = env, %Metadata{} = metadata) do + alias = hint |> List.to_string() |> String.split(".") |> value_from_alias(env, metadata) + + case alias do + {:ok, atom} -> {:ok, {:atom, atom}} + :error -> :error + end + end + + # elixir >= 1.14 + defp expand_dot_path( + {:alias, {:local_or_var, var}, hint}, + %State.Env{} = env, + %Metadata{} = metadata + ) do + case var do + ~c"__MODULE__" -> + alias_suffix = hint |> List.to_string() |> String.split(".") + alias = [{:__MODULE__, [], nil} | alias_suffix] |> value_from_alias(env, metadata) + + case alias do + {:ok, atom} -> {:ok, {:atom, atom}} + :error -> :error + end + + _ -> + :error + end + end + + defp expand_dot_path( + {:alias, {:module_attribute, attribute}, hint}, + %State.Env{} = env, + %Metadata{} = metadata + ) do + case value_from_binding({:attribute, List.to_atom(attribute)}, env, metadata) do + {:ok, {:atom, atom}} -> + if Introspection.elixir_module?(atom) do + alias_suffix = hint |> List.to_string() |> String.split(".") + alias = (Module.split(atom) ++ alias_suffix) |> value_from_alias(env, metadata) + + case alias do + {:ok, atom} -> {:ok, {:atom, atom}} + :error -> :error + end + else + :error + end + + :error -> + :error + end + end + + defp expand_dot_path({:alias, _, _hint}, %State.Env{} = _env, %Metadata{} = _metadata) do + :error + end + + defp expand_dot_path({:unquoted_atom, var}, %State.Env{} = _env, %Metadata{} = _metadata) do + {:ok, {:atom, List.to_atom(var)}} + end + + defp expand_dot_path({:dot, parent, call}, %State.Env{} = env, %Metadata{} = metadata) do + case expand_dot_path(parent, env, metadata) do + {:ok, expanded} -> + value_from_binding({:call, expanded, List.to_atom(call), []}, env, metadata) + + :error -> + :error + end + end + + # elixir >= 1.15 + defp expand_dot_path(:expr, %State.Env{} = _env, %Metadata{} = _metadata) do + # TODO expand expression + :error + end + + defp expand_expr(%State.Env{} = env, %Metadata{} = metadata, cursor_position, opts) do + local_or_var = expand_local_or_var("", env, metadata, cursor_position) + erlang_modules = expand_erlang_modules("", env, metadata) + elixir_modules = expand_aliases("", env, metadata, cursor_position, false, opts) + attributes = expand_attribute("", env, metadata) + + local_or_var ++ erlang_modules ++ elixir_modules ++ attributes + end + + defp no do + [] + end + + ## Formatting + + defp format_expansion(entries) do + Enum.flat_map(entries, &to_entries/1) + end + + defp expand_map_field_access(fields, hint, type, %State.Env{} = env, %Metadata{} = metadata) do + # when there is only one matching field and it's exact to the hint + # and it's not a nested map, iex does not return completions + # We choose to return it normally + match_map_fields(fields, hint, type, env, metadata) + |> format_expansion() + end + + defp expand_require( + mod, + hint, + exact?, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position + ) do + format_expansion( + match_module_funs(mod, hint, exact?, true, :all, env, metadata, cursor_position) + ) + end + + ## Expand local or var + + defp expand_local_or_var(hint, %State.Env{} = env, %Metadata{} = metadata, cursor_position) do + format_expansion( + match_var(hint, env, metadata) ++ match_local(hint, false, env, metadata, cursor_position) + ) + end + + defp expand_local(hint, exact?, %State.Env{} = env, %Metadata{} = metadata, cursor_position) do + format_expansion(match_local(hint, exact?, env, metadata, cursor_position)) + end + + defp expand_var(hint, %State.Env{} = env, %Metadata{} = metadata) do + variables = match_var(hint, env, metadata) + format_expansion(variables) + end + + defp expand_sigil(%State.Env{} = env, %Metadata{} = metadata, cursor_position) do + sigils = + match_local("sigil_", false, env, metadata, cursor_position) + |> Enum.filter(fn %{name: name} -> String.starts_with?(name, "sigil_") end) + |> Enum.map(fn %{name: "sigil_" <> rest} = local -> + %{local | name: "~" <> rest} + end) + + locals = match_local("~", false, env, metadata, cursor_position) + + format_expansion(sigils ++ locals) + end + + defp match_local(hint, exact?, %State.Env{} = env, %Metadata{} = metadata, cursor_position) do + kernel_special_forms_locals = + match_module_funs( + Kernel.SpecialForms, + hint, + exact?, + false, + :all, + env, + metadata, + cursor_position + ) + + current_module_locals = + match_module_funs(env.module, hint, exact?, false, :all, env, metadata, cursor_position) + + imported_locals = + env.imports + |> Introspection.expand_imports(metadata.mods_funs_to_positions) + |> Introspection.combine_imports() + |> Enum.flat_map(fn {scope_import, imported} -> + match_module_funs( + scope_import, + hint, + exact?, + false, + imported, + env, + metadata, + cursor_position + ) + end) + + kernel_special_forms_locals ++ current_module_locals ++ imported_locals + end + + defp match_var(hint, %State.Env{vars: vars}, %Metadata{} = _metadata) do + for( + %State.VarInfo{name: name} when is_atom(name) <- vars, + name = Atom.to_string(name), + Matcher.match?(name, hint), + do: name + ) + |> Enum.sort() + |> Enum.map(&%{kind: :variable, name: &1}) + end + + # do not suggest attributes outside of a module + defp expand_attribute(_, %State.Env{scope: scope}, %Metadata{} = _metadata) + when scope in [Elixir, nil], + do: no() + + defp expand_attribute( + hint, + %State.Env{attributes: attributes, scope: scope}, + %Metadata{} = _metadata + ) do + attribute_names = + attributes + |> Enum.map(fn %State.AttributeInfo{name: name} -> name end) + + attribute_names = + case scope do + {_fun, _arity} -> + attribute_names + + module when not is_nil(module) -> + # include module attributes in module scope + attribute_names ++ BuiltinAttributes.all() + end + + for( + attribute_name when is_atom(attribute_name) <- attribute_names, + name = Atom.to_string(attribute_name), + Matcher.match?(name, hint), + do: attribute_name + ) + |> Enum.sort() + |> Enum.map( + &%{ + kind: :attribute, + name: Atom.to_string(&1), + summary: BuiltinAttributes.docs(&1) + } + ) + |> format_expansion() + end + + ## Erlang modules + + defp expand_erlang_modules(hint, %State.Env{} = env, %Metadata{} = metadata) do + format_expansion(match_erlang_modules(hint, env, metadata)) + end + + defp match_erlang_modules(hint, %State.Env{} = env, %Metadata{} = metadata) do + for mod <- match_modules(hint, true, env, metadata), + usable_as_unquoted_module?(mod) do + mod_as_atom = String.to_atom(mod) + + case :persistent_term.get({@module_results_cache_key, mod_as_atom}, nil) do + nil -> get_erlang_module_result(mod_as_atom) + result -> result + end + end + end + + def fill_erlang_module_cache(module, docs) do + get_erlang_module_result(module, docs) + end + + defp get_erlang_module_result(module, docs \\ nil) do + subtype = Introspection.get_module_subtype(module) + desc = Introspection.get_module_docs_summary(module, docs) + + name = inspect(module) + + result = %{ + kind: :module, + name: name, + full_name: name, + type: :erlang, + desc: desc, + subtype: subtype + } + + :persistent_term.put({@module_results_cache_key, module}, result) + result + end + + defp struct_module_filter(true, %State.Env{} = _env, %Metadata{} = metadata) do + fn module -> Struct.is_struct(module, metadata.structs) end + end + + defp struct_module_filter(false, %State.Env{} = _env, %Metadata{} = _metadata) do + fn _ -> true end + end + + ## Elixir modules + + defp expand_aliases( + all, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position, + only_structs, + opts + ) do + filter = struct_module_filter(only_structs, env, metadata) + + case String.split(all, ".") do + [hint] -> + aliases = match_aliases(hint, env, metadata) + expand_aliases(Elixir, hint, aliases, false, env, metadata, cursor_position, filter, opts) + + parts -> + hint = List.last(parts) + list = Enum.take(parts, length(parts) - 1) + + case value_from_alias(list, env, metadata) do + {:ok, alias} -> + expand_aliases( + alias, + hint, + [], + false, + env, + metadata, + cursor_position, + filter, + Keyword.put(opts, :required_alias, false) + ) + + :error -> + no() + end + end + end + + defp expand_aliases( + mod, + hint, + aliases, + include_funs, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position, + filter, + opts + ) do + aliases + |> Kernel.++(match_elixir_modules(mod, hint, env, metadata, filter, opts)) + |> Kernel.++( + if include_funs, + do: match_module_funs(mod, hint, false, true, :all, env, metadata, cursor_position), + else: [] + ) + |> format_expansion() + end + + defp expand_prefixed_aliases( + {:local_or_var, ~c"__MODULE__"}, + hint, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position, + only_structs + ) do + if env.module != nil and Introspection.elixir_module?(env.module) do + expand_aliases("#{env.module}.#{hint}", env, metadata, cursor_position, only_structs, []) + else + no() + end + end + + defp expand_prefixed_aliases( + {:module_attribute, attribute}, + hint, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position, + only_structs + ) do + case value_from_binding({:attribute, List.to_atom(attribute)}, env, metadata) do + {:ok, {:atom, atom}} -> + if Introspection.elixir_module?(atom) do + expand_aliases("#{atom}.#{hint}", env, metadata, cursor_position, only_structs, []) + else + no() + end + + {:ok, _} -> + # this clause can match e.g. in + # @abc %{SOME: 123} + # @abc.SOME + # but this code does not compile as it defines an invalid alias + no() + + :error -> + no() + end + end + + defp expand_prefixed_aliases( + _, + _hint, + %State.Env{} = _env, + %Metadata{} = _metadata, + _cursor_position, + _only_structs + ), + do: no() + + defp value_from_alias(mod_parts, %State.Env{} = env, %Metadata{} = _metadata) do + mod_parts + |> Enum.map(fn + bin when is_binary(bin) -> String.to_atom(bin) + other -> other + end) + |> Source.concat_module_parts(env.module, env.aliases) + end + + defp match_aliases(hint, %State.Env{} = env, %Metadata{} = _metadata) do + for {alias, mod} <- env.aliases, + [name] = Module.split(alias), + Matcher.match?(name, hint) do + %{ + kind: :module, + type: :elixir, + name: name, + full_name: inspect(mod), + desc: {"", %{}}, + subtype: Introspection.get_module_subtype(mod) + } + end + end + + defp match_elixir_modules( + module, + hint, + %State.Env{} = env, + %Metadata{} = metadata, + filter, + opts + ) do + name = Atom.to_string(module) + depth = length(String.split(name, ".")) + 1 + base = name <> "." <> hint + + concat_module = fn + ["Elixir", "Elixir" | _] = parts -> parts |> tl() |> Module.concat() + parts -> Module.concat(parts) + end + + for mod <- match_modules(base, module === Elixir, env, metadata), + mod_as_atom = mod |> String.to_atom(), + filter.(mod_as_atom), + parts = String.split(mod, "."), + depth <= length(parts), + name = Enum.at(parts, depth - 1), + valid_alias_piece?("." <> name), + concatted = parts |> Enum.take(depth) |> concat_module.(), + filter.(concatted) do + {name, concatted, false} + end + |> Kernel.++( + match_elixir_modules_that_require_alias(module, hint, env, metadata, filter, opts) + ) + |> Enum.reject(fn + {_, concatted, true} -> + Enum.find(env.aliases, fn {_as, module} -> + concatted == module + end) + + _rest -> + false + end) + |> Enum.uniq_by(&elem(&1, 1)) + |> Enum.map(fn {name, module, required_alias?} -> + result = + case metadata.mods_funs_to_positions[{module, nil, nil}] do + nil -> + case :persistent_term.get({@module_results_cache_key, module}, nil) do + nil -> get_elixir_module_result(module) + result -> result + end + + info -> + %{ + kind: :module, + type: :elixir, + full_name: inspect(module), + desc: {Introspection.extract_summary_from_docs(info.doc), info.meta}, + subtype: Metadata.get_module_subtype(metadata, module) + } + end + + result = Map.put(result, :name, name) + + if required_alias? do + Map.put(result, :required_alias, module) + else + result + end + end) + end + + def fill_elixir_module_cache(module, docs) do + get_elixir_module_result(module, docs) + end + + defp get_elixir_module_result(module, docs \\ nil) do + {desc, meta} = Introspection.get_module_docs_summary(module, docs) + subtype = Introspection.get_module_subtype(module) + + result = %{ + kind: :module, + type: :elixir, + full_name: inspect(module), + desc: {desc, meta}, + subtype: subtype + } + + :persistent_term.put({@module_results_cache_key, module}, result) + result + end + + defp valid_alias_piece?(<>) when char in ?A..?Z, + do: valid_alias_rest?(rest) + + defp valid_alias_piece?(_), + do: false + + defp valid_alias_rest?(<>) + when char in ?A..?Z + when char in ?a..?z + when char in ?0..?9 + when char == ?_, + do: valid_alias_rest?(rest) + + defp valid_alias_rest?(<<>>), + do: true + + defp valid_alias_rest?(rest), + do: valid_alias_piece?(rest) + + ## Helpers + + # Version.match? is slow, we need to avoid it in a hot loop + if Version.match?(System.version(), ">= 1.14.0-dev") do + defp usable_as_unquoted_module?(name) do + # Conversion to atom is not a problem because + # it is only called with existing modules names. + # credo:disable-for-lines:7 + Macro.classify_atom(String.to_atom(name)) in [:identifier, :unquoted] and + not String.starts_with?(name, "Elixir.") + end + else + defp usable_as_unquoted_module?(name) do + Code.Identifier.classify(String.to_atom(name)) != :other and + not String.starts_with?(name, "Elixir.") + end + end + + defp match_elixir_modules_that_require_alias( + Elixir, + hint, + %State.Env{} = env, + %Metadata{} = metadata, + filter, + opts + ) do + if Keyword.get(opts, :required_alias) do + for {suggestion, required_alias} <- + find_elixir_modules_that_require_alias(Elixir, hint, env, metadata), + mod_as_atom = required_alias |> String.to_atom(), + filter.(mod_as_atom), + required_alias_mod = required_alias |> String.split(".") |> Module.concat() do + {suggestion, required_alias_mod, true} + end + else + [] + end + end + + defp match_elixir_modules_that_require_alias( + _module, + _hint, + %State.Env{} = _env, + %Metadata{} = _metadata, + _filter, + _opts + ), + do: [] + + defp find_elixir_modules_that_require_alias(Elixir, hint, env, metadata) do + get_modules(true, env, metadata) + |> Enum.sort() + |> Enum.dedup() + |> Enum.reduce([], fn + "Elixir." <> module = full_module, acc -> + subtype = Introspection.get_module_subtype(String.to_atom(full_module)) + # skip mix tasks and protocol implementations as it's not common to need to alias those + # credo:disable-for-next-line + if subtype not in [:implementation, :task] do + # do not search for a match in Elixir. prefix - no need to alias it + module_parts = module |> String.split(".") + + case module_parts do + [_] -> + # no need to alias if module is 1 part + acc + + [_root | rest] -> + rest + |> Enum.with_index(1) + |> Enum.filter(fn {module_part, _index} -> + Matcher.match?(module_part, hint) + end) + |> Enum.reduce(acc, fn {module_part, index}, acc1 -> + required_alias = Enum.slice(module_parts, 0..index) + required_alias = required_alias |> Module.concat() |> Atom.to_string() + + [{module_part, required_alias} | acc1] + end) + end + else + acc + end + + _erlang_module, acc -> + # skip erlang modules + acc + end) + |> Enum.sort() + |> Enum.dedup() + |> Enum.filter(fn {suggestion, _required_alias} -> valid_alias_piece?("." <> suggestion) end) + end + + defp match_modules(hint, root, %State.Env{} = env, %Metadata{} = metadata) do + hint_parts = hint |> String.split(".") + hint_parts_length = length(hint_parts) + [hint_suffix | hint_prefix] = hint_parts |> Enum.reverse() + + root + |> get_modules(env, metadata) + |> Enum.sort() + |> Enum.dedup() + |> Enum.filter(fn mod -> + [mod_suffix | mod_prefix] = + mod |> String.split(".") |> Enum.take(hint_parts_length) |> Enum.reverse() + + hint_prefix == mod_prefix and Matcher.match?(mod_suffix, hint_suffix) + end) + end + + defp get_modules(true, %State.Env{} = env, %Metadata{} = metadata) do + ["Elixir.Elixir"] ++ get_modules(false, env, metadata) + end + + defp get_modules(false, %State.Env{} = env, %Metadata{} = metadata) do + # TODO consider changing this to :code.all_available when otp 23 is required + modules = Enum.map(:code.all_loaded(), &Atom.to_string(elem(&1, 0))) + + # TODO it seems we only run in interactive mode - remove the check? + case :code.get_mode() do + :interactive -> + modules ++ get_modules_from_applications() ++ get_modules_from_metadata(env, metadata) + + _otherwise -> + modules ++ get_modules_from_metadata(env, metadata) + end + end + + defp get_modules_from_applications do + for module <- Applications.get_modules_from_applications() do + Atom.to_string(module) + end + end + + defp get_modules_from_metadata(%State.Env{} = _env, %Metadata{} = metadata) do + for {{k, nil, nil}, _} <- metadata.mods_funs_to_positions, do: Atom.to_string(k) + end + + defp match_module_funs( + mod, + hint, + exact?, + include_builtin, + imported, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position + ) do + falist = + cond do + metadata.mods_funs_to_positions |> Map.has_key?({mod, nil, nil}) -> + get_metadata_module_funs(mod, include_builtin, env, metadata, cursor_position) + + match?({:module, _}, ensure_loaded(mod)) -> + get_module_funs(mod, include_builtin) + + true -> + [] + end + |> Enum.sort_by(fn {f, a, _, _, _, _, _} -> {f, -a} end) + + list = + Enum.reduce(falist, [], fn {f, a, def_a, func_kind, {doc_str, meta}, spec, arg}, acc -> + doc = {Introspection.extract_summary_from_docs(doc_str), meta} + + case :lists.keyfind(f, 1, acc) do + {f, aa, def_arities, func_kinds, docs, specs, args} -> + :lists.keyreplace( + f, + 1, + acc, + {f, [a | aa], [def_a | def_arities], [func_kind | func_kinds], [doc | docs], + [spec | specs], [arg | args]} + ) + + false -> + [{f, [a], [def_a], [func_kind], [doc], [spec], [arg]} | acc] + end + end) + + for {fun, arities, def_arities, func_kinds, docs, specs, args} <- list, + name = Atom.to_string(fun), + if(exact?, do: name == hint, else: Matcher.match?(name, hint)) do + needed_requires = + for func_kind <- func_kinds do + if func_kind in [:macro, :defmacro, :defguard] and mod not in env.requires and + mod != Kernel.SpecialForms do + mod + end + end + + needed_imports = + if imported == :all do + arities |> Enum.map(fn _ -> nil end) + else + arities + |> Enum.map(fn a -> + if {fun, a} not in imported do + {mod, {fun, a}} + end + end) + end + + %{ + kind: :function, + name: name, + arities: arities, + def_arities: def_arities, + module: mod, + func_kinds: func_kinds, + docs: docs, + specs: specs, + needed_requires: needed_requires, + needed_imports: needed_imports, + args: args + } + end + |> Enum.sort_by(& &1.name) + end + + # TODO filter by hint here? + defp get_metadata_module_funs( + mod, + include_builtin, + %State.Env{} = env, + %Metadata{} = metadata, + cursor_position + ) do + case metadata.mods_funs_to_positions[{mod, nil, nil}] do + nil -> + [] + + _funs -> + # local macros are available after definition + # local functions are hoisted + for {{^mod, f, a}, %State.ModFunInfo{} = info} <- metadata.mods_funs_to_positions, + a != nil, + (mod == env.module and not include_builtin) or Introspection.is_pub(info.type), + mod != env.module or State.ModFunInfo.get_category(info) != :macro or + List.last(info.positions) < cursor_position, + include_builtin || {f, a} not in @builtin_functions do + behaviour_implementation = + Metadata.get_module_behaviours(metadata, env, mod) + |> Enum.find_value(fn behaviour -> + if Introspection.is_callback(behaviour, f, a, metadata) do + behaviour + end + end) + + {specs, docs, meta} = + case behaviour_implementation do + nil -> + case metadata.specs[{mod, f, a}] do + nil -> + {"", info.doc, info.meta} + + %State.SpecInfo{specs: specs} -> + {specs |> Enum.reverse() |> Enum.join("\n"), info.doc, info.meta} + end + + behaviour -> + meta = Map.merge(info.meta, %{implementing: behaviour}) + + case metadata.specs[{behaviour, f, a}] do + %State.SpecInfo{} = spec_info -> + specs = spec_info.specs |> Enum.reverse() + + {callback_doc, callback_meta} = + case metadata.mods_funs_to_positions[{behaviour, f, a}] do + nil -> + {spec_info.doc, spec_info.meta} + + def_info -> + # in case of protocol implementation get doc and meta from def + {def_info.doc, def_info.meta} + end + + spec = + specs |> Enum.reject(&String.starts_with?(&1, "@spec")) |> Enum.join("\n") + + {spec, callback_doc, callback_meta |> Map.merge(meta)} + + nil -> + Metadata.get_doc_spec_from_behaviour( + behaviour, + f, + a, + State.ModFunInfo.get_category(info) + ) + end + end + + # assume function head is first in code and last in metadata + head_params = Enum.at(info.params, -1) + args = head_params |> Enum.map(&Macro.to_string/1) + default_args = Introspection.count_defaults(head_params) + + # TODO this is useless - we duplicate and then deduplicate + for arity <- (a - default_args)..a do + {f, arity, a, info.type, {docs, meta}, specs, args} + end + end + |> Enum.concat() + end + end + + # TODO filter by hint here? + def get_module_funs(mod, include_builtin) do + docs = NormalizedCode.get_docs(mod, :docs) + module_specs = TypeInfo.get_module_specs(mod) + + callback_specs = + for behaviour <- Behaviours.get_module_behaviours(mod), + {fa, spec} <- TypeInfo.get_module_callbacks(behaviour), + into: %{}, + do: {fa, {behaviour, spec}} + + if docs != nil and function_exported?(mod, :__info__, 1) do + exports = mod.__info__(:macros) ++ mod.__info__(:functions) ++ special_builtins(mod) + # TODO this is useless - we should only return max arity variant + default_arg_functions = default_arg_functions(docs) + + for {f, a} <- exports do + {f, new_arity} = + case default_arg_functions[{f, a}] do + nil -> {f, a} + new_arity -> {f, new_arity} + end + + {func_kind, func_doc} = find_doc({f, new_arity}, docs) + func_kind = func_kind || :function + + doc = + case func_doc do + nil -> + app = ElixirSense.Core.Applications.get_application(mod) + # TODO provide docs for builtin + if f in [:behaviour_info | @builtin_functions] do + {"", %{builtin: true, app: app}} + else + {"", %{app: app}} + end + + {{_fun, _}, _line, _kind, _args, doc, metadata} -> + {doc, metadata} + end + + spec_key = + case func_kind do + :macro -> {:"MACRO-#{f}", new_arity + 1} + :function -> {f, new_arity} + end + + {_behaviour, fun_spec, spec_kind} = + case callback_specs[spec_key] do + nil -> + {nil, module_specs[spec_key], :spec} + + {behaviour, fun_spec} -> + {behaviour, fun_spec, if(func_kind == :macro, do: :macrocallback, else: :callback)} + end + + spec = Introspection.spec_to_string(fun_spec, spec_kind) + + fun_args = Introspection.extract_fun_args(func_doc) + + # TODO check if this is still needed on 1.13+ + # as of Elixir 1.12 some functions/macros, e.g. Kernel.SpecialForms.fn + # have broken specs in docs + # in that case we fill a dummy fun_args + fun_args = + if length(fun_args) != new_arity do + format_params(nil, new_arity) + else + fun_args + end + + {f, a, new_arity, func_kind, doc, spec, fun_args} + end + |> Kernel.++( + for {f, a} <- @builtin_functions, + include_builtin, + do: {f, a, a, :function, {"", %{}}, nil, nil} + ) + else + funs = + if Code.ensure_loaded?(mod) do + mod.module_info(:exports) + |> Kernel.--(if include_builtin, do: [], else: @builtin_functions) + |> Kernel.++(BuiltinFunctions.erlang_builtin_functions(mod)) + else + [] + end + + for {f, a} <- funs do + # we don't expect macros here + {behaviour, fun_spec} = + case callback_specs[{f, a}] do + nil -> {nil, module_specs[{f, a}]} + callback -> callback + end + + # we load typespec anyway, no big win reading erlang spec from meta[:signature] + + doc_result = + if docs != nil do + {_kind, func_doc} = find_doc({f, a}, docs) + + case func_doc do + nil -> + if behaviour do + {"", %{implementing: behaviour}} + else + {"", %{}} + end + + {{_fun, _}, _line, _kind, _args, doc, metadata} -> + {doc, metadata} + end + else + if behaviour do + {"", %{implementing: behaviour}} + else + {"", %{}} + end + end + + params = format_params(fun_spec, a) + spec = Introspection.spec_to_string(fun_spec, if(behaviour, do: :callback, else: :spec)) + + {f, a, a, :function, doc_result, spec, params} + end + end + end + + defp format_params({{_name, _arity}, [params | _]}, _arity_1) do + TypeInfo.extract_params(params) + end + + defp format_params(nil, 0), do: [] + + defp format_params(nil, arity) do + for _ <- 1..arity, do: "term" + end + + defp special_builtins(mod) do + if Code.ensure_loaded?(mod) do + mod.module_info(:exports) + |> Enum.filter(fn {f, a} -> + {f, a} in [{:behaviour_info, 1}] + end) + else + [] + end + end + + defp find_doc(fun, _docs) when fun in @builtin_functions, do: {:function, nil} + + defp find_doc(fun, docs) do + doc = + docs + |> Enum.find(&match?({^fun, _, _, _, _, _}, &1)) + + case doc do + nil -> {nil, nil} + {_, _, func_kind, _, _, _} = d -> {func_kind, d} + end + end + + defp default_arg_functions(docs) do + for {{fun_name, arity}, _, _kind, args, _, _} <- docs, + count = Introspection.count_defaults(args), + count > 0, + new_arity <- (arity - count)..(arity - 1), + into: %{}, + do: {{fun_name, new_arity}, arity} + end + + defp ensure_loaded(Elixir), do: {:error, :nofile} + defp ensure_loaded(mod), do: Code.ensure_compiled(mod) + + defp match_map_fields(fields, hint, type, %State.Env{} = _env, %Metadata{} = metadata) do + {subtype, origin, types} = + case type do + {:struct, {:atom, mod}} -> + types = + ElixirLS.Utils.Field.get_field_types( + metadata, + mod, + true + ) + + {:struct_field, inspect(mod), types} + + {:struct, nil} -> + {:struct_field, nil, %{}} + + :map -> + {:map_key, nil, %{}} + + other -> + raise "unexpected #{inspect(other)} for hint #{inspect(hint)}" + end + + for {key, value} when is_atom(key) <- fields, + key_str = Atom.to_string(key), + not Regex.match?(~r/^[A-Z]/u, key_str), + Matcher.match?(key_str, hint) do + value_is_map = + case value do + {:map, _, _} -> true + {:struct, _, _, _} -> true + _ -> false + end + + %{ + kind: :field, + name: key_str, + subtype: subtype, + value_is_map: value_is_map, + origin: origin, + type_spec: types[key] + } + end + |> Enum.sort_by(& &1.name) + end + + ## Ad-hoc conversions + @spec to_entries(map) :: [t()] + defp to_entries(%{ + kind: :field, + subtype: subtype, + name: name, + origin: origin, + type_spec: type_spec + }) do + [ + %{ + type: :field, + name: name, + subtype: subtype, + origin: origin, + call?: true, + type_spec: if(type_spec, do: Macro.to_string(type_spec)) + } + ] + end + + defp to_entries( + %{ + kind: :module, + name: name, + full_name: full_name, + desc: {desc, metadata}, + subtype: subtype + } = map + ) do + [ + %{ + type: :module, + name: name, + full_name: full_name, + required_alias: if(map[:required_alias], do: inspect(map[:required_alias])), + subtype: subtype, + summary: desc, + metadata: metadata + } + ] + end + + defp to_entries(%{kind: :variable, name: name}) do + [%{type: :variable, name: name}] + end + + defp to_entries(%{kind: :attribute, name: name, summary: summary}) do + [%{type: :attribute, name: "@" <> name, summary: summary}] + end + + defp to_entries(%{ + kind: :function, + name: name, + arities: arities, + def_arities: def_arities, + needed_imports: needed_imports, + needed_requires: needed_requires, + module: mod, + func_kinds: func_kinds, + docs: docs, + specs: specs, + args: args + }) do + for e <- + Enum.zip([ + arities, + docs, + specs, + args, + def_arities, + func_kinds, + needed_imports, + needed_requires + ]), + {a, {doc, metadata}, spec, args, def_arity, func_kind, needed_import, needed_require} = e do + kind = + case func_kind do + k when k in [:macro, :defmacro, :defmacrop, :defguard, :defguardp] -> :macro + _ -> :function + end + + visibility = + if func_kind in [:defp, :defmacrop, :defguardp] do + :private + else + :public + end + + mod_name = inspect(mod) + + fa = {name |> String.to_atom(), a} + + if fa in (BuiltinFunctions.all() -- [exception: 1, message: 1]) do + args = BuiltinFunctions.get_args(fa) + docs = BuiltinFunctions.get_docs(fa) + + %{ + type: kind, + visibility: visibility, + name: name, + arity: a, + def_arity: def_arity, + args: args |> Enum.join(", "), + args_list: args, + needed_require: nil, + needed_import: nil, + origin: mod_name, + summary: Introspection.extract_summary_from_docs(docs), + metadata: %{builtin: true}, + spec: BuiltinFunctions.get_specs(fa) |> Enum.join("\n"), + snippet: nil + } + else + needed_import = + case needed_import do + nil -> nil + {mod, {fun, arity}} -> {inspect(mod), {Atom.to_string(fun), arity}} + end + + %{ + type: kind, + visibility: visibility, + name: name, + arity: a, + def_arity: def_arity, + args: args |> Enum.join(", "), + args_list: args, + needed_require: if(needed_require, do: inspect(needed_require)), + needed_import: needed_import, + origin: mod_name, + summary: doc, + metadata: metadata, + spec: spec || "", + snippet: nil + } + end + end + end + + defp value_from_binding(binding_ast, %State.Env{} = env, %Metadata{} = metadata) do + case Binding.expand( + Binding.from_env(env, metadata), + binding_ast + ) do + :none -> :error + nil -> :error + other -> {:ok, other} + end + end +end diff --git a/apps/elixir_ls_utils/lib/field.ex b/apps/elixir_ls_utils/lib/field.ex new file mode 100644 index 000000000..26120f59f --- /dev/null +++ b/apps/elixir_ls_utils/lib/field.ex @@ -0,0 +1,62 @@ +defmodule ElixirLS.Utils.Field do + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.State + alias ElixirSense.Core.Normalized.Typespec + alias ElixirSense.Core.TypeInfo + + def get_field_types(%Metadata{} = metadata, mod, include_private) when is_atom(mod) do + case get_field_types_from_metadata(metadata, mod, include_private) do + nil -> get_field_types_from_introspection(mod, include_private) + res -> res + end + end + + defguardp type_is_public(kind, include_private) when kind == :type or include_private + + defp get_field_types_from_metadata( + %Metadata{types: types}, + mod, + include_private + ) do + case types[{mod, :t, 0}] do + %State.TypeInfo{specs: [type_spec], kind: kind} + when type_is_public(kind, include_private) -> + case Code.string_to_quoted(type_spec) do + {:ok, {:@, _, [{_kind, _, [spec]}]}} -> + spec + |> get_fields_from_struct_spec() + + _ -> + nil + end + + _ -> + nil + end + end + + defp get_field_types_from_introspection(nil, _include_private), do: %{} + + defp get_field_types_from_introspection(mod, include_private) when is_atom(mod) do + # assume struct typespec is t() + case TypeInfo.get_type_spec(mod, :t, 0) do + {kind, spec} when type_is_public(kind, include_private) -> + spec + |> Typespec.type_to_quoted() + |> get_fields_from_struct_spec() + + _ -> + %{} + end + end + + defp get_fields_from_struct_spec({:"::", _, [_, {:%, _meta1, [_mod, {:%{}, _meta2, fields}]}]}) do + if Keyword.keyword?(fields) do + Map.new(fields) + else + %{} + end + end + + defp get_fields_from_struct_spec(_), do: %{} +end diff --git a/apps/elixir_ls_utils/lib/matcher.ex b/apps/elixir_ls_utils/lib/matcher.ex new file mode 100644 index 000000000..623403c06 --- /dev/null +++ b/apps/elixir_ls_utils/lib/matcher.ex @@ -0,0 +1,72 @@ +defmodule ElixirLS.Utils.Matcher do + @moduledoc """ + ## Suggestion Matching + """ + + import Kernel, except: [match?: 2] + + @doc """ + Naive sequential fuzzy matching without weight and requiring first char to match. + + ## Examples + + iex> ElixirLS.Utils.Matcher.match?("map", "map") + true + + iex> ElixirLS.Utils.Matcher.match?("map", "m") + true + + iex> ElixirLS.Utils.Matcher.match?("map", "ma") + true + + iex> ElixirLS.Utils.Matcher.match?("map", "mp") + true + + iex> ElixirLS.Utils.Matcher.match?("map", "") + true + + iex> ElixirLS.Utils.Matcher.match?("map", "ap") + false + + iex> ElixirLS.Utils.Matcher.match?("", "") + true + + iex> ElixirLS.Utils.Matcher.match?("chunk_by", "chub") + true + + iex> ElixirLS.Utils.Matcher.match?("chunk_by", "chug") + false + """ + @spec match?(name :: String.t(), hint :: String.t()) :: boolean() + def match?(<>, <>) + when name_head != hint_head do + false + end + + def match?(name, hint) do + do_match?(name, hint) + end + + defp do_match?(<>, <>) do + do_match?(name_rest, hint_rest) + end + + defp do_match?( + <<_head::utf8, name_rest::binary>>, + <<_not_head::utf8, _hint_rest::binary>> = hint + ) do + do_match?(name_rest, hint) + end + + defp do_match?(_name_rest, <<>>) do + true + end + + defp do_match?(<<>>, <<>>) do + true + end + + defp do_match?(<<>>, _) do + false + end +end diff --git a/apps/elixir_ls_utils/mix.exs b/apps/elixir_ls_utils/mix.exs index fe33dc7be..517f56f45 100644 --- a/apps/elixir_ls_utils/mix.exs +++ b/apps/elixir_ls_utils/mix.exs @@ -32,11 +32,12 @@ defmodule ElixirLS.Utils.MixProject do def application do # We must NOT start ANY applications as this is taken care in code. - [applications: [:jason_v]] + [applications: [:jason_v, :elixir_sense]] end defp deps do [ + {:elixir_sense, github: "elixir-lsp/elixir_sense", ref: @dep_versions[:elixir_sense]}, {:jason_v, github: "elixir-lsp/jason", ref: @dep_versions[:jason_v]}, {:mix_task_archive_deps, github: "elixir-lsp/mix_task_archive_deps"}, {:dialyxir_vendored, diff --git a/apps/elixir_ls_utils/test/complete_test.exs b/apps/elixir_ls_utils/test/complete_test.exs new file mode 100644 index 000000000..a51d3bc01 --- /dev/null +++ b/apps/elixir_ls_utils/test/complete_test.exs @@ -0,0 +1,2247 @@ +# This file includes modified code extracted from the elixir project. Namely: +# +# https://github.com/elixir-lang/elixir/blob/v1.9/lib/iex/test/iex/autocomplete_test.exs +# +# The original code is licensed as follows: +# +# Copyright 2012 Plataformatec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule ElixirLS.Utils.CompletionEngineTest do + use ExUnit.Case, async: true + + alias ElixirLS.Utils.CompletionEngine + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.State.Env + alias ElixirSense.Core.State.{ModFunInfo, SpecInfo, VarInfo, AttributeInfo} + + def expand( + expr, + env \\ %Env{ + imports: [{Kernel, []}] + }, + metadata \\ %Metadata{}, + opts \\ [] + ) do + CompletionEngine.do_expand(expr, env, metadata, {1, 1}, opts) + end + + test "erlang module completion" do + assert [ + %{ + name: ":zlib", + full_name: ":zlib", + subtype: nil, + summary: summary, + type: :module, + metadata: metadata + } + ] = expand(~c":zl") + + if System.otp_release() |> String.to_integer() >= 23 do + assert summary =~ "zlib" + assert %{otp_doc_vsn: {1, 0, 0}} = metadata + end + end + + test "erlang module no completion" do + assert expand(~c":unknown") == [] + end + + test "erlang module multiple values completion" do + list = expand(~c":logger") + assert list |> Enum.find(&(&1.name == ":logger")) + assert list |> Enum.find(&(&1.name == ":logger_proxy")) + end + + test "erlang root completion" do + list = expand(~c":") + assert is_list(list) + assert list |> Enum.find(&(&1.name == ":lists")) + assert [] == list |> Enum.filter(&(&1.name |> String.contains?("Elixir.List"))) + end + + test "elixir proxy" do + list = expand(~c"E") + assert list |> Enum.find(&(&1.name == "Elixir" and &1.full_name == "Elixir")) + end + + test "elixir completion" do + assert [_ | _] = expand(~c"En") + + assert [%{name: "Enumerable", full_name: "Enumerable", subtype: :protocol, type: :module}] = + expand(~c"Enumera") + end + + test "elixir module completion with @moduledoc false" do + assert [%{name: "ModuleWithDocFalse", summary: ""}] = + expand(~c"ElixirLS.Utils.Example.ModuleWithDocFals") + end + + test "elixir function completion with @doc false" do + assert [ + %{ + name: "some_fun_doc_false", + summary: "", + args: "a, b \\\\ nil", + arity: 1, + origin: "ElixirLS.Utils.Example.ModuleWithDocs", + spec: "", + type: :function + }, + %{ + args: "a, b \\\\ nil", + arity: 2, + name: "some_fun_doc_false", + origin: "ElixirLS.Utils.Example.ModuleWithDocs", + spec: "", + summary: "", + type: :function + }, + %{ + args: "a, b \\\\ nil", + arity: 1, + name: "some_fun_no_doc", + origin: "ElixirLS.Utils.Example.ModuleWithDocs", + spec: "", + summary: "", + type: :function + }, + %{ + args: "a, b \\\\ nil", + arity: 2, + name: "some_fun_no_doc", + origin: "ElixirLS.Utils.Example.ModuleWithDocs", + spec: "", + summary: "", + type: :function + } + ] = expand(~c"ElixirLS.Utils.Example.ModuleWithDocs.some_fun_") + end + + test "elixir completion with self" do + assert [%{name: "Enumerable", subtype: :protocol}] = expand(~c"Enumerable") + end + + test "elixir completion macro with default args" do + assert [ + %{ + args: "a \\\\ :asdf, b, var \\\\ 0", + arity: 1, + name: "with_default", + origin: "ElixirLS.Utils.Example.BehaviourWithMacrocallback.Impl", + spec: "@spec with_default(atom(), list(), integer()) :: Macro.t()", + summary: "some macro with default arg\n", + type: :macro + }, + %{ + args: "a \\\\ :asdf, b, var \\\\ 0", + arity: 2, + name: "with_default", + origin: "ElixirLS.Utils.Example.BehaviourWithMacrocallback.Impl", + spec: "@spec with_default(atom(), list(), integer()) :: Macro.t()", + summary: "some macro with default arg\n", + type: :macro + }, + %{ + args: "a \\\\ :asdf, b, var \\\\ 0", + arity: 3, + name: "with_default", + origin: "ElixirLS.Utils.Example.BehaviourWithMacrocallback.Impl", + spec: "@spec with_default(atom(), list(), integer()) :: Macro.t()", + summary: "some macro with default arg\n", + type: :macro + } + ] = expand(~c"ElixirLS.Utils.Example.BehaviourWithMacrocallback.Impl.wit") + end + + test "elixir completion on modules from load path" do + assert [ + %{name: "Stream", subtype: :struct, type: :module}, + %{name: "String", subtype: nil, type: :module}, + %{name: "StringIO", subtype: nil, type: :module} + ] = expand(~c"Str") |> Enum.filter(&(&1.name |> String.starts_with?("Str"))) + + assert [ + %{name: "Macro"}, + %{name: "Map"}, + %{name: "MapSet"}, + %{name: "MatchError"} + ] = expand(~c"Ma") |> Enum.filter(&(&1.name |> String.starts_with?("Ma"))) + + assert [%{name: "Dict"}] = + expand(~c"Dic") |> Enum.filter(&(&1.name |> String.starts_with?("Dic"))) + + assert suggestions = expand(~c"Ex") + assert Enum.any?(suggestions, &(&1.name == "ExUnit")) + assert Enum.any?(suggestions, &(&1.name == "Exception")) + end + + test "Elixir no completion for underscored functions with no doc" do + {:module, _, bytecode, _} = + defmodule Elixir.Sample do + def __foo__(), do: 0 + @doc "Bar doc" + def __bar__(), do: 1 + end + + File.write!("Elixir.Sample.beam", bytecode) + + case Code.fetch_docs(Sample) do + {:docs_v1, _, _, _, _, _, _} -> :ok + {:error, :chunk_not_found} -> :ok + end + + # IEx version asserts expansion on Sample._ but we also include :__info__ and there is more than 1 match + assert [%{name: "__bar__"}] = expand(~c"Sample.__b") + after + File.rm("Elixir.Sample.beam") + :code.purge(Sample) + :code.delete(Sample) + end + + test "completion for functions added when compiled module is reloaded" do + {:module, _, bytecode, _} = + defmodule Sample do + def foo(), do: 0 + end + + File.write!("ElixirLS.Utils.CompletionEngineTest.Sample.beam", bytecode) + + assert [%{name: "foo"}] = expand(~c"ElixirLS.Utils.CompletionEngineTest.Sample.foo") + + Code.compiler_options(ignore_module_conflict: true) + + defmodule Sample do + def foo(), do: 0 + def foobar(), do: 0 + end + + assert [%{name: "foo"}, %{name: "foobar"}] = + expand(~c"ElixirLS.Utils.CompletionEngineTest.Sample.foo") + after + File.rm("ElixirLS.Utils.CompletionEngineTest.Sample.beam") + Code.compiler_options(ignore_module_conflict: false) + :code.purge(Sample) + :code.delete(Sample) + end + + test "elixir no completion" do + assert expand(~c".") == [] + assert expand(~c"Xyz") == [] + assert expand(~c"x.Foo") == [] + assert expand(~c"x.Foo.get_by") == [] + # assert expand('@foo.bar') == {:no, '', []} + end + + test "elixir root submodule completion" do + assert [ + %{ + name: "Access", + full_name: "Access", + summary: "Key-based access to data structures." + } + ] = expand(~c"Elixir.Acce") + + assert [_ | _] = expand(~c"Elixir.") + end + + test "elixir submodule completion" do + assert [ + %{ + name: "Chars", + full_name: "String.Chars", + subtype: :protocol, + summary: + "The `String.Chars` protocol is responsible for\nconverting a structure to a binary (only if applicable)." + } + ] = expand(~c"String.Cha") + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "elixir submodule completion with __MODULE__" do + assert [ + %{ + name: "Chars", + full_name: "String.Chars", + subtype: :protocol, + summary: + "The `String.Chars` protocol is responsible for\nconverting a structure to a binary (only if applicable)." + } + ] = expand(~c"__MODULE__.Cha", %Env{module: String}) + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "elixir submodule completion with attribute bound to module" do + assert [ + %{ + name: "Chars", + full_name: "String.Chars", + subtype: :protocol, + summary: + "The `String.Chars` protocol is responsible for\nconverting a structure to a binary (only if applicable)." + } + ] = + expand(~c"@my_attr.Cha", %Env{ + attributes: [ + %AttributeInfo{ + name: :my_attr, + type: {:atom, String} + } + ] + }) + end + end + + test "find elixir modules that require alias" do + assert [ + %{ + metadata: %{}, + name: "Chars", + full_name: "List.Chars", + required_alias: "List.Chars" + }, + %{ + metadata: %{}, + name: "Chars", + full_name: "String.Chars", + required_alias: "String.Chars" + } + ] = expand(~c"Char", %Env{}, %Metadata{}, required_alias: true) + end + + test "does not suggest required_alias when alias already exists" do + env = %Env{ + aliases: [{MyChars, String.Chars}] + } + + results = expand(~c"Char", env, %Metadata{}, required_alias: true) + + refute Enum.find(results, fn expansion -> expansion[:required_alias] == String.Chars end) + end + + test "does not suggest required_alias for Elixir proxy" do + env = %Env{ + aliases: [] + } + + results = expand(~c"Elixi", env, %Metadata{}, required_alias: true) + + refute Enum.find(results, fn expansion -> expansion[:required_alias] == Elixir end) + end + + test "does not suggest required_alias for when hint has more than one part" do + results = expand(~c"Elixir.Char", %Env{}, %Metadata{}, required_alias: true) + + refute Enum.find(results, fn expansion -> expansion[:required_alias] == String.Chars end) + + results = expand(~c"String.", %Env{}, %Metadata{}, required_alias: true) + + refute Enum.find(results, fn expansion -> + expansion[:required_alias] == String.Tokenizer.Security + end) + end + + test "elixir submodule no completion" do + assert expand(~c"IEx.Xyz") == [] + end + + test "function completion" do + assert [%{name: "version", origin: "System"}] = expand(~c"System.ve") + assert [%{name: "fun2ms", origin: ":ets"}] = expand(~c":ets.fun2") + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "function completion on __MODULE__" do + assert [%{name: "version", origin: "System"}] = + expand(~c"__MODULE__.ve", %Env{module: System}) + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "function completion on __MODULE__ submodules" do + assert [%{name: "to_string", origin: "String.Chars"}] = + expand(~c"__MODULE__.Chars.to", %Env{module: String}) + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "function completion on attribute bound to module" do + assert [%{name: "version", origin: "System"}] = + expand(~c"@my_attr.ve", %Env{ + attributes: [ + %AttributeInfo{ + name: :my_attr, + type: {:atom, System} + } + ] + }) + end + end + + test "function completion with arity" do + assert [ + %{ + name: "printable?", + arity: 1, + spec: + "@spec printable?(t(), 0) :: true\n@spec printable?(t(), pos_integer() | :infinity) :: boolean()", + summary: + "Checks if a string contains only printable characters up to `character_limit`." + }, + %{ + name: "printable?", + arity: 2, + spec: + "@spec printable?(t(), 0) :: true\n@spec printable?(t(), pos_integer() | :infinity) :: boolean()", + summary: + "Checks if a string contains only printable characters up to `character_limit`." + } + ] = expand(~c"String.printable?") + + assert [%{name: "printable?", arity: 1}, %{name: "printable?", arity: 2}] = + expand(~c"String.printable?/") + + assert [ + %{ + name: "count", + arity: 1 + }, + %{ + name: "count", + arity: 2 + }, + %{ + name: "count_until", + arity: 2 + }, + %{ + name: "count_until", + arity: 3 + } + ] = expand(~c"Enum.count") + + assert [ + %{ + name: "count", + arity: 1 + }, + %{ + name: "count", + arity: 2 + } + ] = expand(~c"Enum.count/") + end + + test "operator completion" do + assert [%{name: "+", arity: 1}, %{name: "+", arity: 2}, %{name: "++", arity: 2}] = + expand(~c"+") + + assert [%{name: "+", arity: 1}, %{name: "+", arity: 2}] = expand(~c"+/") + assert [%{name: "++", arity: 2}] = expand(~c"++/") + + assert entries = expand(~c"+ ") + assert entries |> Enum.any?(&(&1.name == "div")) + end + + test "sigil completion" do + sigils = expand(~c"~") + assert sigils |> Enum.any?(fn s -> s.name == "~C" end) + # We choose not to provide sigil quotations + # {:yes, '', sigils} = expand('~r') + # assert '"' in sigils + # assert '(' in sigils + assert [] == expand(~c"~r") + end + + test "function completion using a variable bound to a module" do + env = %Env{ + vars: [ + %VarInfo{ + name: :mod, + type: {:atom, String} + } + ] + } + + assert [%{name: "printable?", arity: 1}, %{name: "printable?", arity: 2}] = + expand(~c"mod.print", env) + end + + test "map atom key completion is supported" do + env = %Env{ + vars: [ + %VarInfo{ + name: :map, + type: {:map, [foo: 1, bar_1: 23, bar_2: 14], nil} + } + ] + } + + assert expand(~c"map.f", env) == + [ + %{ + name: "foo", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert [_ | _] = expand(~c"map.b", env) + + assert expand(~c"map.bar_", env) == + [ + %{ + name: "bar_1", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "bar_2", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"map.c", env) == [] + + assert expand(~c"map.", env) == + [ + %{ + name: "bar_1", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "bar_2", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "foo", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"map.foo", env) == [ + %{ + call?: true, + name: "foo", + origin: nil, + subtype: :map_key, + type: :field, + type_spec: nil + } + ] + end + + test "struct key completion is supported" do + env = %Env{ + vars: [ + %VarInfo{ + name: :struct, + type: {:struct, [], {:atom, DateTime}, nil} + }, + %VarInfo{ + name: :other, + type: {:call, {:atom, DateTime}, :utc_now, []} + }, + %VarInfo{ + name: :from_metadata, + type: {:struct, [], {:atom, MyStruct}, nil} + }, + %VarInfo{ + name: :var, + type: {:variable, :struct} + }, + %VarInfo{ + name: :yyyy, + type: {:map, [date: {:struct, [], {:atom, DateTime}, nil}], []} + }, + %VarInfo{ + name: :xxxx, + type: {:call, {:atom, Map}, :fetch!, [{:variable, :yyyy}, {:atom, :date}]} + } + ] + } + + metadata = %Metadata{ + types: %{ + {MyStruct, :t, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :t, + args: [[]], + specs: ["@type t :: %MyStruct{some: integer}"], + kind: :type + } + }, + structs: %{ + MyStruct => %ElixirSense.Core.State.StructInfo{type: :defstruct, fields: [some: 1]} + } + } + + assert expand(~c"struct.h", env, metadata) == + [ + %{ + call?: true, + name: "hour", + origin: "DateTime", + subtype: :struct_field, + type: :field, + type_spec: "Calendar.hour()" + } + ] + + assert expand(~c"other.d", env, metadata) == + [ + %{ + call?: true, + name: "day", + origin: "DateTime", + subtype: :struct_field, + type: :field, + type_spec: "Calendar.day()" + } + ] + + assert expand(~c"from_metadata.s", env, metadata) == + [ + %{ + call?: true, + name: "some", + origin: "MyStruct", + subtype: :struct_field, + type: :field, + type_spec: "integer" + } + ] + + assert expand(~c"var.h", env, metadata) == + [ + %{ + call?: true, + name: "hour", + origin: "DateTime", + subtype: :struct_field, + type: :field, + type_spec: "Calendar.hour()" + } + ] + + assert expand(~c"xxxx.h", env, metadata) == + [ + %{ + call?: true, + name: "hour", + origin: "DateTime", + subtype: :struct_field, + type: :field, + type_spec: "Calendar.hour()" + } + ] + end + + test "map atom key completion is supported on attributes" do + env = %Env{ + attributes: [ + %AttributeInfo{ + name: :map, + type: {:map, [foo: 1, bar_1: 23, bar_2: 14], nil} + } + ] + } + + assert expand(~c"@map.f", env) == + [ + %{ + name: "foo", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert [_ | _] = expand(~c"@map.b", env) + + assert expand(~c"@map.bar_", env) == + [ + %{ + name: "bar_1", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "bar_2", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"@map.c", env) == [] + + assert expand(~c"@map.", env) == + [ + %{ + name: "bar_1", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "bar_2", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "foo", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"@map.foo", env) == [ + %{ + call?: true, + name: "foo", + origin: nil, + subtype: :map_key, + type: :field, + type_spec: nil + } + ] + end + + test "nested map atom key completion is supported" do + env = %Env{ + vars: [ + %VarInfo{ + name: :map, + type: + {:map, + [ + nested: + {:map, + [ + deeply: + {:map, + [ + foo: 1, + bar_1: 23, + bar_2: 14, + mod: {:atom, String}, + num: 1 + ], nil} + ], nil} + ], nil} + } + ] + } + + assert expand(~c"map.nested.deeply.f", env) == + [ + %{ + name: "foo", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert [_ | _] = expand(~c"map.nested.deeply.b", env) + + assert expand(~c"map.nested.deeply.bar_", env) == + [ + %{ + name: "bar_1", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "bar_2", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"map.nested.deeply.", env) == + [ + %{ + name: "bar_1", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "bar_2", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "foo", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "mod", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + }, + %{ + name: "num", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert [_ | _] = expand(~c"map.nested.deeply.mod.print", env) + + assert expand(~c"map.nested", env) == + [ + %{ + name: "nested", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"map.nested.deeply", env) == + [ + %{ + name: "deeply", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"map.nested.deeply.foo", env) == [ + %{ + call?: true, + name: "foo", + origin: nil, + subtype: :map_key, + type: :field, + type_spec: nil + } + ] + + assert expand(~c"map.nested.deeply.c", env) == [] + assert expand(~c"map.a.b.c.f", env) == [] + end + + test "map string key completion is not supported" do + env = %Env{ + vars: [ + %VarInfo{ + name: :map, + type: {:map, [{"foo", 124}], nil} + } + ] + } + + assert expand(~c"map.f", env) == [] + end + + test "autocompletion off a bound variable only works for modules and maps" do + env = %Env{ + vars: [ + %VarInfo{ + name: :map, + type: {:map, [nested: {:map, [num: 23], nil}], nil} + } + ] + } + + assert expand(~c"num.print", env) == [] + assert expand(~c"map.nested.num.f", env) == [] + assert expand(~c"map.nested.num.key.f", env) == [] + end + + test "autocomplete map fields from call binding" do + env = %Env{ + vars: [ + %VarInfo{ + name: :map, + type: {:map, [{:foo, {:atom, String}}], nil} + }, + %VarInfo{ + name: :call, + type: {:call, {:variable, :map}, :foo, []} + } + ] + } + + assert [_ | _] = expand(~c"call.print", env) + end + + test "autocomplete call return binding" do + env = %Env{ + vars: [ + %VarInfo{ + name: :call, + type: {:call, {:atom, DateTime}, :utc_now, []} + } + ] + } + + assert [_ | _] = expand(~c"call.ho", env) + assert [_ | _] = expand(~c"DateTime.utc_now.ho", env) + # TODO expand expression {:dot, :expr, []} {:dot, :expr, ~c"ho"} on 1.15+ + # Code.cursor_context returns :none for those cases + assert [] == expand(~c"DateTime.utc_now().", env) + assert [] == expand(~c"DateTime.utc_now().ho", env) + assert [] == expand(~c"DateTime.utc_now().calendar.da", env) + end + + test "autocompletion off of unbound variables is not supported" do + assert expand(~c"other_var.f") == [] + assert expand(~c"a.b.c.d") == [] + end + + test "macro completion" do + assert [_ | _] = expand(~c"Kernel.is_") + end + + test "imports completion" do + list = expand(~c"") + assert is_list(list) + + assert list |> Enum.find(&(&1.name == "unquote")) + # IEX version asserts IEx.Helpers are imported + # assert list |> Enum.find(& &1.name == "h") + # assert list |> Enum.find(& &1.name == "pwd") + end + + test "imports completion in call arg" do + # local call + list = expand(~c"asd(") + assert is_list(list) + + assert list |> Enum.find(&(&1.name == "unquote")) + + list = expand(~c"asd(un") + assert is_list(list) + + assert list |> Enum.find(&(&1.name == "unquote")) + + # remote call + + list = expand(~c"Abc.asd(") + assert is_list(list) + + assert list |> Enum.find(&(&1.name == "unquote")) + + list = expand(~c"Abc.asd(un") + assert is_list(list) + + assert list |> Enum.find(&(&1.name == "unquote")) + + # local call on var + + if Version.match?(System.version(), "< 1.16.0-dev") do + assert [] == expand(~c"asd.(") + assert [] == expand(~c"@asd.(") + else + expr_suggestions = expand(~c"") + assert expr_suggestions == expand(~c"asd.(") + assert expr_suggestions == expand(~c"@asd.(") + end + + # list = expand('asd.(') + # assert is_list(list) + + # assert list |> Enum.find(&(&1.name == "unquote")) + + list = expand(~c"asd.(un") + assert is_list(list) + + assert list |> Enum.find(&(&1.name == "unquote")) + end + + test "kernel import completion" do + assert [ + %{ + args: "fields", + arity: 1, + name: "defstruct", + origin: "Kernel", + spec: "", + summary: "Defines a struct.", + type: :macro + } + ] = expand(~c"defstru") + + assert [ + %{arity: 3, name: "put_elem"}, + %{arity: 2, name: "put_in"}, + %{arity: 3, name: "put_in"} + ] = expand(~c"put_") + end + + test "variable name completion" do + env = %Env{ + vars: [ + %VarInfo{ + name: :numeral + }, + %VarInfo{ + name: :number + }, + %VarInfo{ + name: :nothing + } + ] + } + + assert expand(~c"numb", env) == [%{type: :variable, name: "number"}] + + assert expand(~c"num", env) == + [%{type: :variable, name: "number"}, %{type: :variable, name: "numeral"}] + + assert [%{type: :variable, name: "nothing"} | _] = expand(~c"no", env) + end + + test "variable name completion after pin" do + env = %Env{ + vars: [ + %VarInfo{ + name: :number + } + ] + } + + assert expand(~c"^numb", env) == [%{type: :variable, name: "number"}] + assert expand(~c"^", env) == [%{type: :variable, name: "number"}] + end + + test "attribute name completion" do + env = %Env{ + attributes: [ + %AttributeInfo{ + name: :numeral + }, + %AttributeInfo{ + name: :number + }, + %AttributeInfo{ + name: :nothing + } + ], + scope: {:some, 0} + } + + assert expand(~c"@numb", env) == [%{type: :attribute, name: "@number", summary: nil}] + + assert expand(~c"@num", env) == + [ + %{type: :attribute, name: "@number", summary: nil}, + %{type: :attribute, name: "@numeral", summary: nil} + ] + + assert expand(~c"@", env) == + [ + %{name: "@nothing", type: :attribute, summary: nil}, + %{type: :attribute, name: "@number", summary: nil}, + %{type: :attribute, name: "@numeral", summary: nil} + ] + end + + test "builtin attribute name completion" do + env_function = %Env{ + attributes: [], + scope: {:some, 0} + } + + env_module = %Env{ + attributes: [], + scope: Some.Module + } + + env_outside_module = %Env{ + attributes: [], + scope: Elixir + } + + assert expand(~c"@befo", env_function) == [] + assert expand(~c"@befo", env_outside_module) == [] + + assert expand(~c"@befo", env_module) == + [ + %{ + type: :attribute, + name: "@before_compile", + summary: "A hook that will be invoked before the module is compiled." + } + ] + end + + test "kernel special form completion" do + assert [%{name: "unquote_splicing", origin: "Kernel.SpecialForms"}] = expand(~c"unquote_spl") + end + + test "completion inside expression" do + assert [_ | _] = expand(~c"1 En") + assert [_ | _] = expand(~c"Test(En") + assert [_] = expand(~c"Test :zl") + assert [_] = expand(~c"[:zl") + assert [_] = expand(~c"{:zl") + end + + test "ampersand completion" do + assert [_ | _] = expand(~c"&Enu") + + assert [ + %{name: "all?", arity: 1}, + %{name: "all?", arity: 2}, + %{name: "any?", arity: 1}, + %{name: "any?", arity: 2}, + %{name: "at", arity: 2}, + %{name: "at", arity: 3} + ] = expand(~c"&Enum.a") + + assert [ + %{name: "all?", arity: 1}, + %{name: "all?", arity: 2}, + %{name: "any?", arity: 1}, + %{name: "any?", arity: 2}, + %{name: "at", arity: 2}, + %{name: "at", arity: 3} + ] = expand(~c"f = &Enum.a") + end + + defmodule SublevelTest.LevelA.LevelB do + end + + test "elixir completion sublevel" do + assert [%{name: "LevelA"}] = + expand(~c"ElixirLS.Utils.CompletionEngineTest.SublevelTest.") + end + + defmodule MyServer do + def current_env do + %Macro.Env{aliases: [{MyList, List}, {EList, :lists}]} + end + end + + test "complete aliases of elixir modules" do + env = %Env{ + aliases: [{MyList, List}] + } + + assert [%{name: "MyList"}] = expand(~c"MyL", env) + assert [%{name: "MyList"}] = expand(~c"MyList", env) + + assert [%{arity: 1, name: "to_integer"}, %{arity: 2, name: "to_integer"}] = + expand(~c"MyList.to_integer", env) + end + + test "complete aliases of erlang modules" do + env = %Env{ + aliases: [{ErpList, :lists}] + } + + assert [%{name: "ErpList"}] = expand(~c"ErpL", env) + assert [%{name: "ErpList"}] = expand(~c"ErpList", env) + + assert [ + %{arity: 2, name: "map"}, + %{arity: 3, name: "mapfoldl"}, + %{arity: 3, name: "mapfoldr"} + ] = expand(~c"ErpList.map", env) + end + + test "complete local funs from scope module" do + env = %Env{ + module: MyModule + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {MyModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {MyModule, :my_fun_priv, nil} => %ModFunInfo{type: :defp}, + {MyModule, :my_fun_priv, 2} => %ModFunInfo{ + type: :defp, + params: [[{:some, [], nil}, {:other, [], nil}]] + }, + {MyModule, :my_fun_pub, nil} => %ModFunInfo{type: :def}, + {MyModule, :my_fun_pub, 1} => %ModFunInfo{type: :def, params: [[{:some, [], nil}]]}, + {MyModule, :my_macro_priv, nil} => %ModFunInfo{type: :defmacrop}, + {MyModule, :my_macro_priv, 1} => %ModFunInfo{ + type: :defmacrop, + params: [[{:some, [], nil}]] + }, + {MyModule, :my_macro_pub, nil} => %ModFunInfo{type: :defmacro}, + {MyModule, :my_macro_pub, 1} => %ModFunInfo{type: :defmacro, params: [[{:some, [], nil}]]}, + {MyModule, :my_guard_priv, nil} => %ModFunInfo{type: :defguardp}, + {MyModule, :my_guard_priv, 1} => %ModFunInfo{ + type: :defguardp, + params: [[{:some, [], nil}]] + }, + {MyModule, :my_guard_pub, nil} => %ModFunInfo{type: :defguard}, + {MyModule, :my_guard_pub, 1} => %ModFunInfo{type: :defguard, params: [[{:some, [], nil}]]}, + {MyModule, :my_delegated, nil} => %ModFunInfo{type: :defdelegate}, + {MyModule, :my_delegated, 1} => %ModFunInfo{ + type: :defdelegate, + params: [[{:some, [], nil}]] + }, + {OtherModule, nil, nil} => %ModFunInfo{}, + {OtherModule, :my_fun_pub_other, nil} => %ModFunInfo{type: :def}, + {OtherModule, :my_fun_pub_other, 1} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + } + }, + specs: %{ + {MyModule, :my_fun_priv, 2} => %SpecInfo{ + kind: :spec, + specs: ["@spec my_fun_priv(atom, integer) :: boolean"] + } + } + } + + assert [_ | _] = expand(~c"my_f", env, metadata) + + assert [ + %{ + name: "my_fun_priv", + origin: "MyModule", + args: "some, other", + type: :function, + spec: "@spec my_fun_priv(atom, integer) :: boolean" + } + ] = expand(~c"my_fun_pr", env, metadata) + + assert [ + %{name: "my_fun_pub", origin: "MyModule", type: :function} + ] = expand(~c"my_fun_pu", env, metadata) + + assert [ + %{name: "my_macro_priv", origin: "MyModule", type: :macro} + ] = expand(~c"my_macro_pr", env, metadata) + + assert [ + %{name: "my_macro_pub", origin: "MyModule", type: :macro} + ] = expand(~c"my_macro_pu", env, metadata) + + assert [ + %{name: "my_guard_priv", origin: "MyModule", type: :macro} + ] = expand(~c"my_guard_pr", env, metadata) + + assert [ + %{name: "my_guard_pub", origin: "MyModule", type: :macro} + ] = expand(~c"my_guard_pu", env, metadata) + + assert [ + %{name: "my_delegated", origin: "MyModule", type: :function} + ] = expand(~c"my_de", env, metadata) + end + + test "complete remote funs from imported module" do + env = %Env{ + module: MyModule, + imports: [{OtherModule, []}, {Kernel, []}] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {OtherModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {OtherModule, :my_fun_other_pub, nil} => %ModFunInfo{type: :def}, + {OtherModule, :my_fun_other_pub, 1} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + }, + {OtherModule, :my_fun_other_priv, nil} => %ModFunInfo{type: :defp}, + {OtherModule, :my_fun_other_priv, 1} => %ModFunInfo{ + type: :defp, + params: [[{:some, [], nil}]] + } + } + } + + assert [ + %{name: "my_fun_other_pub", origin: "OtherModule", needed_import: nil} + ] = expand(~c"my_f", env, metadata) + end + + test "complete remote funs from imported module - needed import" do + env = %Env{ + module: MyModule, + imports: [{OtherModule, [only: [{:my_fun_other_pub, 1}]]}, {Kernel, []}] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {OtherModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {OtherModule, :my_fun_other_pub, nil} => %ModFunInfo{type: :def}, + {OtherModule, :my_fun_other_pub, 1} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + }, + {OtherModule, :my_fun_other_pub, 2} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + }, + {OtherModule, :my_fun_other_priv, nil} => %ModFunInfo{type: :defp}, + {OtherModule, :my_fun_other_priv, 1} => %ModFunInfo{ + type: :defp, + params: [[{:some, [], nil}]] + } + } + } + + assert [ + %{name: "my_fun_other_pub", origin: "OtherModule", needed_import: nil}, + %{ + name: "my_fun_other_pub", + origin: "OtherModule", + needed_import: {"OtherModule", {"my_fun_other_pub", 2}} + } + ] = expand(~c"my_f", env, metadata) + end + + test "complete remote funs" do + env = %Env{ + module: MyModule + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {Some.OtherModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {Some.OtherModule, :my_fun_other_pub, nil} => %ModFunInfo{type: :def}, + {Some.OtherModule, :my_fun_other_pub, 1} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + }, + {Some.OtherModule, :my_fun_other_priv, nil} => %ModFunInfo{type: :defp}, + {Some.OtherModule, :my_fun_other_priv, 1} => %ModFunInfo{ + type: :defp, + params: [[{:some, [], nil}]] + } + } + } + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand(~c"Some.OtherModule.my_f", env, metadata) + end + + test "complete remote funs from aliased module" do + env = %Env{ + module: MyModule, + aliases: [{S, Some.OtherModule}] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {Some.OtherModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {Some.OtherModule, :my_fun_other_pub, nil} => %ModFunInfo{type: :def}, + {Some.OtherModule, :my_fun_other_pub, 1} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + }, + {Some.OtherModule, :my_fun_other_priv, nil} => %ModFunInfo{type: :defp}, + {Some.OtherModule, :my_fun_other_priv, 1} => %ModFunInfo{ + type: :defp, + params: [[{:some, [], nil}]] + } + } + } + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand(~c"S.my_f", env, metadata) + end + + test "complete remote funs from injected module" do + env = %Env{ + module: MyModule, + attributes: [ + %AttributeInfo{ + name: :get_module, + type: + {:call, {:atom, Application}, :get_env, + [atom: :elixir_sense, atom: :an_attribute, atom: Some.OtherModule]} + }, + %AttributeInfo{ + name: :compile_module, + type: + {:call, {:atom, Application}, :compile_env, + [atom: :elixir_sense, atom: :an_attribute, atom: Some.OtherModule]} + }, + %AttributeInfo{ + name: :fetch_module, + type: + {:call, {:atom, Application}, :fetch_env!, + [atom: :elixir_sense, atom: :other_attribute]} + }, + %AttributeInfo{ + name: :compile_bang_module, + type: + {:call, {:atom, Application}, :compile_env!, + [atom: :elixir_sense, atom: :other_attribute]} + } + ] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {Some.OtherModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {Some.OtherModule, :my_fun_other_pub, nil} => %ModFunInfo{type: :def}, + {Some.OtherModule, :my_fun_other_pub, 1} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + }, + {Some.OtherModule, :my_fun_other_priv, nil} => %ModFunInfo{type: :defp}, + {Some.OtherModule, :my_fun_other_priv, 1} => %ModFunInfo{ + type: :defp, + params: [[{:some, [], nil}]] + } + } + } + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand(~c"@get_module.my_f", env, metadata) + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand(~c"@compile_module.my_f", env, metadata) + + Application.put_env(:elixir_sense, :other_attribute, Some.OtherModule) + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand(~c"@fetch_module.my_f", env, metadata) + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand(~c"@compile_bang_module.my_f", env, metadata) + after + Application.delete_env(:elixir_sense, :other_attribute) + end + + test "complete modules" do + env = %Env{ + module: MyModule, + aliases: [{MyAlias, Some.OtherModule.Nested}] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {Some.OtherModule, nil, nil} => %ModFunInfo{type: :defmodule} + } + } + + assert [%{name: "Some", full_name: "Some", type: :module}] = expand(~c"Som", env, metadata) + + assert [%{name: "OtherModule", full_name: "Some.OtherModule", type: :module}] = + expand(~c"Some.", env, metadata) + + assert [%{name: "MyAlias", full_name: "Some.OtherModule.Nested", type: :module}] = + expand(~c"MyA", env, metadata) + end + + test "alias rules" do + env = %Env{ + module: MyModule, + aliases: [{Keyword, MyKeyword}] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {MyKeyword, nil, nil} => %ModFunInfo{type: :defmodule}, + {MyKeyword, :values1, 0} => %ModFunInfo{type: :def, params: [[]]}, + {MyKeyword, :values1, nil} => %ModFunInfo{type: :def} + } + } + + assert [ + %{ + name: "values1", + type: :function, + args: "", + arity: 0, + origin: "MyKeyword", + spec: "", + summary: "" + } + ] = expand(~c"Keyword.valu", env, metadata) + + assert [%{name: "values", type: :function, arity: 1, origin: "Keyword"}] = + expand(~c"Elixir.Keyword.valu", env, metadata) + end + + defmodule MyStruct do + defstruct [:my_val, :some_map, :a_mod, :str, :unknown_str] + end + + test "completion for struct names" do + assert [%{name: "MyStruct"}] = + expand(~c"%ElixirLS.Utils.CompletionEngineTest.MyStr") + + assert entries = expand(~c"%") + assert entries |> Enum.any?(&(&1.name == "URI")) + + assert [%{name: "MyStruct"}] = expand(~c"%ElixirLS.Utils.CompletionEngineTest.") + + env = %Env{ + aliases: [{MyDate, Date}] + } + + entries = expand(~c"%My", env, %Metadata{}, required_alias: true) + assert Enum.any?(entries, &(&1.name == "MyDate" and &1.subtype == :struct)) + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "completion for struct names with __MODULE__" do + assert [%{name: "__MODULE__"}] = expand(~c"%__MODU", %Env{module: Date.Range}) + assert [%{name: "Range"}] = expand(~c"%__MODULE__.Ra", %Env{module: Date}) + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "completion for struct attributes" do + assert [%{name: "@my_attr"}] = + expand(~c"%@my", %Env{ + attributes: [ + %AttributeInfo{ + name: :my_attr, + type: {:atom, Date} + } + ], + scope: MyMod + }) + + assert [%{name: "Range"}] = + expand(~c"%@my_attr.R", %Env{ + attributes: [ + %AttributeInfo{ + name: :my_attr, + type: {:atom, Date} + } + ], + scope: MyMod + }) + end + end + + # handled elsewhere + # TODO consider moving struct key completion here after elixir 1.13+ is required + # test "completion for struct keys" do + # assert {:yes, '', entries} = expand('%URI{') + # assert 'path:' in entries + # assert 'query:' in entries + + # assert {:yes, '', entries} = expand('%URI{path: "foo",') + # assert 'path:' not in entries + # assert 'query:' in entries + + # assert {:yes, 'ry: ', []} = expand('%URI{path: "foo", que') + # assert {:no, [], []} = expand('%URI{path: "foo", unkno') + # assert {:no, [], []} = expand('%Unkown{path: "foo", unkno') + # end + + test "completion for struct keys" do + env = %Env{ + vars: [ + %VarInfo{ + name: :struct, + type: + {:struct, + [ + a_mod: {:atom, String}, + some_map: {:map, [asdf: 1], nil}, + str: {:struct, [], {:atom, MyStruct}, nil}, + unknown_str: {:struct, [abc: nil], nil, nil} + ], {:atom, MyStruct}, nil} + } + ] + } + + assert expand(~c"struct.my", env) == + [ + %{ + name: "my_val", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + } + ] + + assert expand(~c"struct.some_m", env) == + [ + %{ + name: "some_map", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + } + ] + + assert expand(~c"struct.some_map.", env) == + [ + %{ + name: "asdf", + subtype: :map_key, + type: :field, + origin: nil, + call?: true, + type_spec: nil + } + ] + + assert expand(~c"struct.str.", env) == + [ + %{ + name: "__struct__", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + }, + %{ + name: "a_mod", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + }, + %{ + name: "my_val", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + }, + %{ + name: "some_map", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + }, + %{ + name: "str", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + }, + %{ + name: "unknown_str", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + } + ] + + assert expand(~c"struct.str", env) == + [ + %{ + name: "str", + subtype: :struct_field, + type: :field, + origin: "ElixirLS.Utils.CompletionEngineTest.MyStruct", + call?: true, + type_spec: nil + } + ] + + assert expand(~c"struct.unknown_str.", env) == + [ + %{ + call?: true, + name: "__struct__", + origin: nil, + subtype: :struct_field, + type: :field, + type_spec: nil + }, + %{ + call?: true, + name: "abc", + origin: nil, + subtype: :struct_field, + type: :field, + type_spec: nil + } + ] + end + + test "ignore invalid Elixir module literals" do + defmodule :"ElixirSense.Providers.Suggestion.CompleteTest.Unicodé", do: nil + assert expand(~c"ElixirLS.Utils.CompletionEngineTest.Unicod") == [] + after + :code.purge(:"ElixirLS.Utils.CompletionEngineTest.Unicodé") + :code.delete(:"ElixirLS.Utils.CompletionEngineTest.Unicodé") + end + + test "complete built in functions on non local calls" do + assert [] = expand(~c"module_") + assert [] = expand(~c"__in") + + assert [] = expand(~c"Elixir.mo") + assert [] = expand(~c"Elixir.__in") + + assert [ + %{ + name: "module_info", + type: :function, + arity: 0, + spec: + "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]" + }, + %{ + name: "module_info", + type: :function, + arity: 1, + spec: + "@spec module_info(:module) :: atom\n@spec module_info(:attributes | :compile) :: [{atom, term}]\n@spec module_info(:md5) :: binary\n@spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}]\n@spec module_info(:native) :: boolean" + } + ] = expand(~c"String.mo") + + assert [ + %{ + name: "__info__", + type: :function, + spec: + "@spec __info__(:attributes) :: keyword()\n@spec __info__(:compile) :: [term()]\n@spec __info__(:functions) :: [{atom, non_neg_integer}]\n@spec __info__(:macros) :: [{atom, non_neg_integer}]\n@spec __info__(:md5) :: binary()\n@spec __info__(:module) :: module()" + } + ] = expand(~c"String.__in") + + assert [ + %{ + name: "module_info", + type: :function, + arity: 0, + spec: + "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]" + }, + %{ + name: "module_info", + type: :function, + arity: 1, + spec: + "@spec module_info(:module) :: atom\n@spec module_info(:attributes | :compile) :: [{atom, term}]\n@spec module_info(:md5) :: binary\n@spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}]\n@spec module_info(:native) :: boolean" + } + ] = expand(~c":ets.module_") + + assert [] = expand(~c":ets.__in") + + env = %Env{ + module: MyModule, + aliases: [{MyAlias, Some.OtherModule.Nested}] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {MyModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {MyModule, :module_info, nil} => %ModFunInfo{type: :def}, + {MyModule, :module_info, 0} => %ModFunInfo{type: :def, params: [[]]}, + {MyModule, :module_info, 1} => %ModFunInfo{type: :def, params: [[{:atom, [], nil}]]}, + {MyModule, :__info__, nil} => %ModFunInfo{type: :def}, + {MyModule, :__info__, 1} => %ModFunInfo{type: :def, params: [[{:atom, [], nil}]]} + } + } + + assert [] = expand(~c"module_", env, metadata) + assert [] = expand(~c"__in", env, metadata) + + assert [ + %{ + name: "module_info", + type: :function, + arity: 0, + spec: + "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]" + }, + %{ + name: "module_info", + type: :function, + arity: 1, + spec: + "@spec module_info(:module) :: atom\n@spec module_info(:attributes | :compile) :: [{atom, term}]\n@spec module_info(:md5) :: binary\n@spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}]\n@spec module_info(:native) :: boolean" + } + ] = expand(~c"MyModule.mo", env, metadata) + + assert [ + %{ + name: "__info__", + type: :function, + spec: + "@spec __info__(:attributes) :: keyword()\n@spec __info__(:compile) :: [term()]\n@spec __info__(:functions) :: [{atom, non_neg_integer}]\n@spec __info__(:macros) :: [{atom, non_neg_integer}]\n@spec __info__(:md5) :: binary()\n@spec __info__(:module) :: module()" + } + ] = expand(~c"MyModule.__in", env, metadata) + end + + test "complete build in behaviour functions" do + assert [] = expand(~c"Elixir.beh") + + assert [ + %{ + name: "behaviour_info", + type: :function, + arity: 1, + spec: + "@spec behaviour_info(:callbacks | :optional_callbacks) :: [{atom, non_neg_integer}]" + } + ] = expand(~c":gen_server.beh") + + assert [ + %{ + name: "behaviour_info", + type: :function, + arity: 1, + spec: + "@spec behaviour_info(:callbacks | :optional_callbacks) :: [{atom, non_neg_integer}]" + } + ] = expand(~c"GenServer.beh") + end + + test "complete build in protocol functions" do + assert [] = expand(~c"Elixir.__pr") + + assert [ + %{ + name: "__protocol__", + type: :function, + arity: 1, + spec: + "@spec __protocol__(:module) :: module\n@spec __protocol__(:functions) :: [{atom, non_neg_integer}]\n@spec __protocol__(:consolidated?) :: boolean\n@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}" + } + ] = expand(~c"Enumerable.__pro") + + assert [_, _] = expand(~c"Enumerable.imp") + + assert [ + %{ + name: "impl_for!", + type: :function, + arity: 1, + spec: "@spec impl_for!(term) :: atom" + } + ] = expand(~c"Enumerable.impl_for!") + end + + test "complete build in protocol implementation functions" do + assert [] = expand(~c"Elixir.__im") + + assert [ + %{ + name: "__impl__", + type: :function, + arity: 1, + spec: "@spec __impl__(:for | :target | :protocol) :: module" + } + ] = expand(~c"Enumerable.List.__im") + end + + test "complete build in struct functions" do + assert [] = expand(~c"Elixir.__str") + + assert [ + %{ + name: "__struct__", + type: :function, + arity: 0, + spec: + "@spec __struct__() :: %{required(:__struct__) => module, optional(any) => any}" + }, + %{ + name: "__struct__", + type: :function, + arity: 1, + spec: + "@spec __struct__(keyword) :: %{required(:__struct__) => module, optional(any) => any}" + } + ] = expand(~c"ElixirLS.Utils.Example.ModuleWithStruct.__str") + end + + test "complete build in exception functions" do + assert [] = expand(~c"Elixir.mes") + + assert [ + %{ + name: "message", + type: :function, + arity: 1, + spec: "@callback message(t()) :: String.t()" + } + ] = expand(~c"ArgumentError.mes") + + assert [] = expand(~c"Elixir.exce") + + assert [ + %{ + name: "exception", + type: :function, + arity: 1, + spec: "@callback exception(term()) :: t()" + } + ] = expand(~c"ArgumentError.exce") + + assert [] = expand(~c"Elixir.bla") + + assert [ + %{name: "blame", type: :function, arity: 2} + ] = expand(~c"ArgumentError.bla") + end + + if System.otp_release() |> String.to_integer() >= 23 do + test "complete build in :erlang functions" do + assert [ + %{arity: 2, name: "open_port", origin: ":erlang"}, + %{ + arity: 2, + name: "or", + spec: "@spec boolean() or boolean() :: boolean()", + type: :function, + args: "boolean, boolean", + origin: ":erlang", + summary: "" + }, + %{ + args: "term, term", + arity: 2, + name: "orelse", + origin: ":erlang", + spec: "", + summary: "", + type: :function + } + ] = expand(~c":erlang.or") + + assert [ + %{ + arity: 2, + name: "and", + spec: "@spec boolean() and boolean() :: boolean()", + type: :function, + args: "boolean, boolean", + origin: ":erlang", + summary: "" + }, + %{ + args: "term, term", + arity: 2, + name: "andalso", + origin: ":erlang", + spec: "", + summary: "", + type: :function + }, + %{arity: 2, name: "append", origin: ":erlang"}, + %{arity: 2, name: "append_element", origin: ":erlang"} + ] = expand(~c":erlang.and") + end + end + + test "provide doc and specs for erlang functions" do + assert [ + %{ + arity: 1, + name: "whereis", + origin: ":erlang", + spec: "@spec whereis(regName) :: pid() | port() | :undefined when regName: atom()", + type: :function + } + ] = expand(~c":erlang.where") + + assert [ + %{ + arity: 1, + name: "cancel_timer", + spec: "@spec cancel_timer(timerRef) :: result" <> _, + type: :function, + args: "timerRef", + origin: ":erlang", + summary: summary1 + }, + %{ + arity: 2, + name: "cancel_timer", + spec: "@spec cancel_timer(timerRef, options) :: result | :ok" <> _, + type: :function, + args: "timerRef, options", + origin: ":erlang", + summary: summary2 + } + ] = expand(~c":erlang.cancel_time") + + if System.otp_release() |> String.to_integer() >= 23 do + assert "Cancels a timer\\." <> _ = summary1 + assert "Cancels a timer that has been created by" <> _ = summary2 + end + end + + test "provide doc and specs for erlang functions with args from typespec" do + if String.to_integer(System.otp_release()) >= 26 do + assert [ + %{ + name: "handle_call", + args_list: ["call", "from", "state"] + }, + %{ + name: "handle_cast", + args_list: ["tuple", "state"] + }, + %{ + name: "handle_info", + args_list: ["term", "state"] + } + ] = expand(~c":pg.handle_") + else + if String.to_integer(System.otp_release()) >= 23 do + assert [_, _, _] = expand(~c":pg.handle_") + else + assert [] = expand(~c":pg.handle_") + end + end + end + + test "complete after ! operator" do + assert [%{name: "is_binary"}] = expand(~c"!is_bina") + end + + test "correctly find subtype and doc for modules that have submodule" do + assert [ + %{ + name: "File", + full_name: "File", + type: :module, + metadata: %{}, + subtype: nil, + summary: "This module contains functions to manipulate files." + } + ] = expand(~c"Fi") |> Enum.filter(&(&1.name == "File")) + end + + test "complete only struct modules after %" do + assert list = expand(~c"%") + refute Enum.any?(list, &(&1.type != :module)) + assert Enum.any?(list, &(&1.name == "ArithmeticError")) + assert Enum.any?(list, &(&1.name == "URI")) + refute Enum.any?(list, &(&1.name == "File")) + refute Enum.any?(list, &(&1.subtype not in [:struct, :exception])) + + assert [_ | _] = expand(~c"%Fi") + assert list = expand(~c"%File.") + assert Enum.any?(list, &(&1.name == "CopyError")) + refute Enum.any?(list, &(&1.type != :module)) + refute Enum.any?(list, &(&1.subtype not in [:struct, :exception])) + end + + test "complete modules and local funs after &" do + assert list = expand(~c"&") + assert Enum.any?(list, &(&1.type == :module)) + assert Enum.any?(list, &(&1.type == :function)) + refute Enum.any?(list, &(&1.type not in [:function, :module, :macro])) + end + + test "complete Kernel.SpecialForms macros with fixed argument list" do + assert [%{args_list: ["term"]}] = expand(~c"Kernel.SpecialForms.fn") + end + + test "macros from not required modules should add needed_require" do + assert [ + %{ + name: "info", + arity: 1, + type: :macro, + origin: "Logger", + needed_require: "Logger", + visibility: :public + }, + _ + ] = expand(~c"Logger.inf") + + assert [ + %{ + name: "info", + arity: 1, + type: :macro, + origin: "Logger", + needed_require: nil, + visibility: :public + }, + _ + ] = expand(~c"Logger.inf", %Env{requires: [Logger]}) + end + + test "macros from not required metadata modules should add needed_require" do + macro_info = %ElixirSense.Core.State.ModFunInfo{ + type: :defmacro, + params: [[:_]] + } + + metadata = %Metadata{ + mods_funs_to_positions: %{ + {MyModule, nil, nil} => %ElixirSense.Core.State.ModFunInfo{}, + {MyModule, :info, nil} => macro_info, + {MyModule, :info, 1} => macro_info + } + } + + assert [ + %{ + name: "info", + arity: 1, + type: :macro, + origin: "MyModule", + needed_require: "MyModule", + visibility: :public + } + ] = expand(~c"MyModule.inf", %Env{requires: []}, metadata) + + assert [ + %{ + name: "info", + arity: 1, + type: :macro, + origin: "MyModule", + needed_require: nil, + visibility: :public + } + ] = expand(~c"MyModule.inf", %Env{requires: [MyModule]}, metadata) + end + + test "macros from Kernel.SpecialForms should not add needed_require" do + assert [ + %{ + name: "unquote", + arity: 1, + type: :macro, + origin: "Kernel.SpecialForms", + needed_require: nil, + visibility: :public + }, + _ + ] = expand(~c"unquote", %Env{requires: []}) + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "Application.compile_env classified as macro" do + assert [ + %{ + name: "compile_env", + arity: 2, + type: :macro, + origin: "Application", + needed_require: "Application" + }, + %{ + name: "compile_env", + arity: 3, + type: :macro, + origin: "Application", + needed_require: "Application" + }, + %{ + name: "compile_env", + arity: 4, + type: :function, + origin: "Application", + needed_require: nil + }, + %{ + name: "compile_env!", + arity: 2, + type: :macro, + origin: "Application", + needed_require: "Application" + }, + %{ + name: "compile_env!", + arity: 3, + type: :function, + origin: "Application", + needed_require: nil + } + ] = expand(~c"Application.compile_e") + end + end +end diff --git a/apps/elixir_ls_utils/test/matcher_test.exs b/apps/elixir_ls_utils/test/matcher_test.exs new file mode 100644 index 000000000..fbdb3e130 --- /dev/null +++ b/apps/elixir_ls_utils/test/matcher_test.exs @@ -0,0 +1,4 @@ +defmodule ElixirLS.Utils.MatcherTest do + use ExUnit.Case, async: true + doctest ElixirLS.Utils.Matcher +end diff --git a/apps/elixir_ls_utils/test/support/behaviour_with_macrocallbacks.ex b/apps/elixir_ls_utils/test/support/behaviour_with_macrocallbacks.ex new file mode 100644 index 000000000..4ea34e85b --- /dev/null +++ b/apps/elixir_ls_utils/test/support/behaviour_with_macrocallbacks.ex @@ -0,0 +1,33 @@ +defmodule ElixirLS.Utils.Example.BehaviourWithMacrocallback do + @doc """ + A required macrocallback + """ + @macrocallback required(atom) :: Macro.t() + + @doc """ + An optional macrocallback + """ + @macrocallback optional(a) :: Macro.t() when a: atom + + @optional_callbacks [optional: 1] +end + +defmodule ElixirLS.Utils.Example.BehaviourWithMacrocallback.Impl do + @behaviour ElixirLS.Utils.Example.BehaviourWithMacrocallback + defmacro required(var), do: Macro.expand(var, __CALLER__) + defmacro optional(var), do: Macro.expand(var, __CALLER__) + + @doc """ + some macro + """ + @spec some(integer) :: Macro.t() + @spec some(b) :: Macro.t() when b: float + + defmacro some(var), do: Macro.expand(var, __CALLER__) + + @doc """ + some macro with default arg + """ + @spec with_default(atom, list, integer) :: Macro.t() + defmacro with_default(a \\ :asdf, b, var \\ 0), do: Macro.expand({a, b, var}, __CALLER__) +end diff --git a/apps/elixir_ls_utils/test/support/module_with_struct.ex b/apps/elixir_ls_utils/test/support/module_with_struct.ex new file mode 100644 index 000000000..5266d962a --- /dev/null +++ b/apps/elixir_ls_utils/test/support/module_with_struct.ex @@ -0,0 +1,11 @@ +defmodule ElixirLS.Utils.Example.ModuleWithStruct do + defstruct [:field_1, field_2: 1] +end + +defmodule ElixirLS.Utils.Example.ModuleWithTypedStruct do + @type t :: %ElixirLS.Utils.Example.ModuleWithTypedStruct{ + typed_field: %ElixirLS.Utils.Example.ModuleWithStruct{}, + other: integer + } + defstruct [:typed_field, other: 1] +end diff --git a/apps/elixir_ls_utils/test/support/modules_with_docs.ex b/apps/elixir_ls_utils/test/support/modules_with_docs.ex new file mode 100644 index 000000000..4318c08de --- /dev/null +++ b/apps/elixir_ls_utils/test/support/modules_with_docs.ex @@ -0,0 +1,123 @@ +defmodule ElixirLS.Utils.Example.ModuleWithDocs do + @moduledoc """ + An example module + """ + @moduledoc since: "1.2.3" + + @typedoc """ + An example type + """ + @typedoc since: "1.1.0" + @type some_type :: integer + @typedoc false + @type some_type_doc_false :: integer + @type some_type_no_doc :: integer + + @typedoc """ + An example opaque type + """ + @opaque opaque_type :: integer + + @doc """ + An example fun + """ + @doc since: "1.1.0" + def some_fun(a, b \\ nil), do: a + b + @doc false + def some_fun_doc_false(a, b \\ nil), do: a + b + def some_fun_no_doc(a, b \\ nil), do: a + b + + @doc """ + An example macro + """ + @doc since: "1.1.0" + defmacro some_macro(a, b \\ nil), do: a + b + @doc false + defmacro some_macro_doc_false(a, b \\ nil), do: a + b + defmacro some_macro_no_doc(a, b \\ nil), do: a + b + + @doc """ + An example callback + """ + @doc since: "1.1.0" + @callback some_callback(integer) :: atom + @doc false + @callback some_callback_doc_false(integer) :: atom + @callback some_callback_no_doc(integer) :: atom + + @doc """ + An example callback + """ + @doc since: "1.1.0" + @macrocallback some_macrocallback(integer) :: atom + @doc false + @macrocallback some_macrocallback_doc_false(integer) :: atom + @macrocallback some_macrocallback_no_doc(integer) :: atom + + @doc """ + An example fun + """ + @doc deprecated: "This function will be removed in a future release" + def soft_deprecated_fun(_a), do: :ok + + @doc """ + An example macro + """ + @doc deprecated: "This macro will be removed in a future release" + defmacro soft_deprecated_macro(_a), do: :ok + + # As of elixir 1.10 hard deprecation by @deprecated attribute is only supported for macros and functions + + @doc """ + An example fun + """ + @deprecated "This function will be removed in a future release" + def hard_deprecated_fun(_a), do: :ok + + @doc """ + An example macro + """ + @deprecated "This macro will be removed in a future release" + defmacro hard_deprecated_macro(_a), do: :ok + + @doc """ + An example callback + """ + @doc deprecated: "This callback will be removed in a future release" + @callback soft_deprecated_callback(integer) :: atom + + @doc """ + An example macrocallback + """ + @doc deprecated: "This callback will be removed in a future release" + @macrocallback soft_deprecated_macrocallback(integer) :: atom + + @typedoc """ + An example type + """ + @typedoc deprecated: "This type will be removed in a future release" + @type soft_deprecated_type :: integer + + @optional_callbacks soft_deprecated_callback: 1, soft_deprecated_macrocallback: 1 +end + +defmodule ElixirLS.Utils.Example.ModuleWithDocFalse do + @moduledoc false +end + +defmodule ElixirLS.Utils.Example.ModuleWithNoDocs do +end + +defmodule ElixirLS.Utils.Example.SoftDeprecatedModule do + @moduledoc """ + An example module + """ + @moduledoc deprecated: "This module will be removed in a future release" +end + +defmodule ElixirLS.Utils.Example.ModuleWithDelegates do + @doc """ + A delegated function + """ + defdelegate delegated_fun(a, b), to: ElixirLS.Utils.Example.ModuleWithDocs, as: :some_fun_no_doc +end diff --git a/apps/language_server/.formatter.exs b/apps/language_server/.formatter.exs index 2ebc7f334..4c4e4cafe 100644 --- a/apps/language_server/.formatter.exs +++ b/apps/language_server/.formatter.exs @@ -9,7 +9,8 @@ impossible_to_format = [ "project_with_tests", "test", "error_test.exs" - ]) + ]), + Path.join(current_directory, "test/support/modules_with_references.ex") ] deps = diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 7e04d1361..44b9ad858 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -328,7 +328,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do 4 other -> - Logger.warn( + Logger.warning( "Invalid severity on diagnostic: #{inspect(other)}, using warning level" ) diff --git a/apps/language_server/lib/language_server/dialyzer.ex b/apps/language_server/lib/language_server/dialyzer.ex index de3ac9c5b..f3d782dbb 100644 --- a/apps/language_server/lib/language_server/dialyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer.ex @@ -490,7 +490,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do # Analyze! Logger.info( "[ElixirLS Dialyzer] Analyzing #{Enum.count(modules_to_analyze)} modules: " <> - "#{inspect(modules_to_analyze)}" + "#{inspect(Enum.sort(modules_to_analyze))}" ) {active_plt, new_mod_deps, raw_warnings} = Analyzer.analyze(active_plt, files_to_analyze) diff --git a/apps/language_server/lib/language_server/location.ex b/apps/language_server/lib/language_server/location.ex new file mode 100644 index 000000000..88077e6c7 --- /dev/null +++ b/apps/language_server/lib/language_server/location.ex @@ -0,0 +1,362 @@ +defmodule ElixirLS.LanguageServer.Location do + @moduledoc """ + A location in a source file or buffer + """ + + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Parser + alias ElixirSense.Core.Source + alias ElixirSense.Core.State.ModFunInfo + alias ElixirSense.Core.Normalized.Code, as: CodeNormalized + require ElixirSense.Core.Introspection, as: Introspection + + @type t :: %__MODULE__{ + type: :module | :function | :variable | :typespec | :macro | :attribute, + file: String.t() | nil, + line: pos_integer, + column: pos_integer + } + defstruct [:type, :file, :line, :column] + + @spec find_mod_fun_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) :: + t() | nil + def find_mod_fun_source(mod, fun, arity) do + case find_mod_file(mod) do + file when is_binary(file) -> + find_fun_position({mod, file}, fun, arity) + + _ -> + nil + end + end + + @spec find_type_source(module, atom, non_neg_integer | {:gte, non_neg_integer} | :any) :: + t() | nil + def find_type_source(mod, type, arity) do + case find_mod_file(mod) do + file when is_binary(file) -> + find_type_position({mod, file}, type, arity) + + _ -> + nil + end + end + + 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 + nil -> nil + source -> List.to_string(source) + end + end + + if file do + if File.exists?(file, [:raw]) do + file + else + # 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(:language_server, :elixir_src), + file = + String.replace( + file, + Regex.recompile!(~r<^(?:.+)(/lib/.+\.ex)$>U), + elixir_src <> "\\1" + ), + true <- File.exists?(file, [:raw]) do + file + else + _ -> nil + end + end + end + end + + 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 + result = + if String.ends_with?(file, ".erl") do + # erlang function docs point to `-spec` instead of function + # module docs point to the begin of a file + # we get better results by regex + # the downside is we don't handle arity well + find_fun_position_in_erl_file(file, fun) + else + %Metadata{mods_funs_to_positions: mods_funs_to_positions} = + Parser.parse_file(file, false, false, nil) + + case get_function_position_using_metadata(mod, fun, arity, mods_funs_to_positions) do + %ModFunInfo{} = mi -> + # assume function head or first clause is last in metadata + {List.last(mi.positions), ModFunInfo.get_category(mi)} + + nil -> + # not found in metadata, fall back to docs + get_function_position_using_docs(mod, fun, arity) + end + end + + case result do + {{line, column}, category} -> + %__MODULE__{type: category, file: file, line: line, column: column} + + _ -> + nil + end + end + + defp find_fun_position_in_erl_file(file, nil) do + case find_line_by_regex(file, ~r/^-module/u) do + nil -> nil + position -> {position, :module} + end + end + + defp find_fun_position_in_erl_file(file, name) do + escaped = + name + |> Atom.to_string() + |> Regex.escape() + + case find_line_by_regex(file, ~r/^#{escaped}\b/u) do + nil -> nil + position -> {position, :function} + end + end + + defp find_type_position_in_erl_file(file, name) do + escaped = + name + |> Atom.to_string() + |> Regex.escape() + + find_line_by_regex(file, ~r/^-(typep?|opaque)\s#{escaped}\b/u) + end + + defp find_line_by_regex(file, regex) do + index = + file + |> File.read!() + |> Source.split_lines() + |> Enum.find_index(&String.match?(&1, regex)) + + case index do + nil -> nil + i -> {i + 1, 1} + end + end + + defp find_type_position(_, nil, _), do: nil + + defp find_type_position({mod, file}, name, arity) do + result = + if String.ends_with?(file, ".erl") do + find_type_position_in_erl_file(file, name) + else + file_metadata = Parser.parse_file(file, false, false, nil) + get_type_position(file_metadata, mod, name, arity) + end + + case result do + {line, column} -> + %__MODULE__{type: :typespec, file: file, line: line, column: column} + + _ -> + nil + end + end + + defp get_function_position_using_docs(module, nil, _) do + case CodeNormalized.fetch_docs(module) do + {:error, _} -> + nil + + {_, anno, _, _, _, _, _} -> + line = :erl_anno.line(anno) + + line = + if line == 0 do + 1 + else + line + end + + column = :erl_anno.column(anno) + + column = + if column == :undefined do + 1 + else + column + end + + {{line, column}, :module} + end + end + + defp get_function_position_using_docs(module, function, arity) do + case CodeNormalized.fetch_docs(module) do + {:error, _} -> + nil + + {_, _, _, _, _, _, docs} -> + docs + |> Enum.filter(fn + {{category, ^function, doc_arity}, _line, _, _, meta} + when category in [:function, :macro] -> + default_args = Map.get(meta, :defaults, 0) + Introspection.matches_arity_with_defaults?(doc_arity, default_args, arity) + + _ -> + false + end) + |> Enum.map(fn + {{category, _function, _arity}, line, _, _, _} when is_integer(line) -> + {{line, 1}, category} + + {{category, _function, _arity}, keyword, _, _, _} when is_list(keyword) -> + {{Keyword.get(keyword, :location, 1), 1}, category} + end) + |> Enum.min_by(fn {{line, 1}, _category} -> line end, &<=/2, fn -> nil end) + end + end + + def get_type_position(metadata, module, type, arity) do + case get_type_position_using_metadata(module, type, arity, metadata.types) do + nil -> + get_type_position_using_docs(module, type, arity) + + %ElixirSense.Core.State.TypeInfo{positions: positions} -> + List.last(positions) + end + end + + def get_type_position_using_docs(module, type_name, arity) do + case CodeNormalized.fetch_docs(module) do + {:error, _} -> + nil + + {_, _, _, _, _, _, docs} -> + docs + |> Enum.filter(fn + {{:type, ^type_name, doc_arity}, _line, _, _, _meta} -> + Introspection.matches_arity?(doc_arity, arity) + + _ -> + false + end) + |> Enum.map(fn + {{_category, _function, _arity}, line, _, _, _} when is_integer(line) -> + {line, 1} + + {{_category, _function, _arity}, keyword, _, _, _} when is_list(keyword) -> + {Keyword.get(keyword, :location, 1), 1} + end) + |> Enum.min_by(fn {line, 1} -> line end, &<=/2, fn -> nil end) + end + end + + def get_function_position_using_metadata( + mod, + fun, + call_arity, + mods_funs_to_positions, + predicate \\ fn _ -> true end + ) + + def get_function_position_using_metadata( + mod, + nil, + _call_arity, + mods_funs_to_positions, + predicate + ) do + mods_funs_to_positions + |> Enum.find_value(fn + {{^mod, nil, nil}, fun_info} -> + if predicate.(fun_info) do + fun_info + end + + _ -> + false + end) + end + + def get_function_position_using_metadata( + mod, + fun, + call_arity, + mods_funs_to_positions, + predicate + ) do + mods_funs_to_positions + |> Enum.filter(fn + {{^mod, ^fun, fn_arity}, fun_info} when not is_nil(fn_arity) -> + # assume function head is first in code and last in metadata + default_args = fun_info.params |> Enum.at(-1) |> Introspection.count_defaults() + + Introspection.matches_arity_with_defaults?(fn_arity, default_args, call_arity) and + predicate.(fun_info) + + _ -> + false + end) + |> min_by_line + end + + def get_type_position_using_metadata(mod, fun, call_arity, types, predicate \\ fn _ -> true end) do + types + |> Enum.filter(fn + {{^mod, ^fun, type_arity}, type_info} + when not is_nil(type_arity) and Introspection.matches_arity?(type_arity, call_arity) -> + predicate.(type_info) + + _ -> + false + end) + |> min_by_line + end + + defp min_by_line(list) do + result = + list + |> Enum.min_by( + fn {_, %{positions: positions}} -> + positions |> List.last() |> elem(0) + end, + &<=/2, + fn -> nil end + ) + + case result do + {_, info} -> info + nil -> nil + end + end +end diff --git a/apps/language_server/lib/language_server/protocol/location.ex b/apps/language_server/lib/language_server/protocol/location.ex index e63568286..a33a98f11 100644 --- a/apps/language_server/lib/language_server/protocol/location.ex +++ b/apps/language_server/lib/language_server/protocol/location.ex @@ -11,7 +11,7 @@ defmodule ElixirLS.LanguageServer.Protocol.Location do require ElixirLS.LanguageServer.Protocol, as: Protocol def new( - %ElixirSense.Location{file: file, line: line, column: column}, + %ElixirLS.LanguageServer.Location{file: file, line: line, column: column}, current_file_uri, current_file_text, project_dir diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 5b950b6ed..600bf87a2 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -10,7 +10,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do alias ElixirLS.LanguageServer.Protocol.TextEdit alias ElixirLS.LanguageServer.{SourceFile, Parser} import ElixirLS.LanguageServer.Protocol, only: [range: 4] - alias ElixirSense.Providers.Suggestion.Matcher + alias ElixirLS.Utils.Matcher alias ElixirSense.Core.Normalized.Code, as: NormalizedCode alias ElixirLS.LanguageServer.MarkdownUtils require Logger @@ -251,7 +251,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do required_alias = Keyword.get(options, :auto_insert_required_alias, true) items = - ElixirSense.suggestions(text, line, character, + ElixirLS.LanguageServer.Providers.Completion.Suggestion.suggestions(text, line, character, required_alias: required_alias, metadata: metadata ) diff --git a/apps/language_server/lib/language_server/providers/completion/generic_reducer.ex b/apps/language_server/lib/language_server/providers/completion/generic_reducer.ex new file mode 100644 index 000000000..af4b897ea --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/generic_reducer.ex @@ -0,0 +1,92 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.GenericReducer do + @moduledoc """ + A generic behaviour for reducers that customize suggestions + according to the cursor's position in a function call. + """ + + require Logger + # TODO change/move this + alias ElixirSense.Plugins.Util + + @type func_call :: {module, fun :: atom, arg :: non_neg_integer, any} + @type suggestion :: ElixirLS.LanguageServer.Providers.Completion.Suggestion.generic() + @type reducer_name :: atom() + + @callback suggestions(hint :: String.t(), func_call, [func_call], opts :: map) :: + :ignore + | {:add | :override, [suggestion]} + | {:add | :override, [suggestion], [reducer_name]} + + defmacro __using__(_) do + quote do + @behaviour unquote(__MODULE__) + + def reduce(hint, env, buffer_metadata, cursor_context, acc) do + unquote(__MODULE__).reduce(__MODULE__, hint, env, buffer_metadata, cursor_context, acc) + end + end + end + + def reduce(reducer, hint, env, buffer_metadata, cursor_context, acc) do + text_before = cursor_context.text_before + + opts = %{ + env: env, + buffer_metadata: buffer_metadata, + cursor_context: cursor_context, + module_store: acc.context.module_store + } + + case Util.func_call_chain(text_before, env, buffer_metadata) do + [func_call | _] = chain -> + if function_exported?(reducer, :suggestions, 4) do + try do + reducer.suggestions(hint, func_call, chain, opts) |> handle_suggestions(acc) + catch + kind, payload -> + {payload, stacktrace} = Exception.blame(kind, payload, __STACKTRACE__) + message = Exception.format(kind, payload, stacktrace) + Logger.error("Error in suggestions reducer: #{message}") + {:cont, acc} + end + else + {:cont, acc} + end + + [] -> + if function_exported?(reducer, :suggestions, 2) do + try do + reducer.suggestions(hint, opts) |> handle_suggestions(acc) + catch + kind, payload -> + {payload, stacktrace} = Exception.blame(kind, payload, __STACKTRACE__) + message = Exception.format(kind, payload, stacktrace) + Logger.error("Error in suggestions reducer: #{message}") + {:cont, acc} + end + else + {:cont, acc} + end + end + end + + def handle_suggestions(:ignore, acc) do + {:cont, acc} + end + + def handle_suggestions({:add, suggestions}, acc) do + {:cont, %{acc | result: suggestions ++ acc.result}} + end + + def handle_suggestions({:add, suggestions, reducers}, acc) do + {:cont, %{acc | result: suggestions ++ acc.result, reducers: reducers}} + end + + def handle_suggestions({:override, suggestions}, acc) do + {:halt, %{acc | result: suggestions}} + end + + def handle_suggestions({:override, suggestions, reducers}, acc) do + {:cont, %{acc | result: suggestions, reducers: reducers}} + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducer.ex b/apps/language_server/lib/language_server/providers/completion/reducer.ex new file mode 100644 index 000000000..8d9040446 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducer.ex @@ -0,0 +1,14 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducer do + @moduledoc !""" + Provides common functions for reducers. + """ + + def put_context(acc, key, value) do + updated_context = Map.put(acc.context, key, value) + put_in(acc.context, updated_context) + end + + def get_context(acc, key) do + get_in(acc, [:context, key]) + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/bitstring.ex b/apps/language_server/lib/language_server/providers/completion/reducers/bitstring.ex new file mode 100644 index 000000000..7c8f2c881 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/bitstring.ex @@ -0,0 +1,39 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Bitstring do + @moduledoc false + + alias ElixirSense.Core.Bitstring + alias ElixirSense.Core.Source + + @type bitstring_option :: %{ + type: :bitstring_option, + name: String.t() + } + + @doc """ + A reducer that adds suggestions of bitstring options. + """ + def add_bitstring_options(_hint, _env, _buffer_metadata, cursor_context, acc) do + prefix = cursor_context.text_before + + case Source.bitstring_options(prefix) do + candidate when not is_nil(candidate) -> + parsed = Bitstring.parse(candidate) + + list = + for option <- Bitstring.available_options(parsed), + candidate_part = candidate |> String.split("-") |> List.last(), + option_str = option |> Atom.to_string(), + String.starts_with?(option_str, candidate_part) do + %{ + name: option_str, + type: :bitstring_option + } + end + + {:cont, %{acc | result: acc.result ++ list}} + + _ -> + {:cont, acc} + end + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/callbacks.ex b/apps/language_server/lib/language_server/providers/completion/reducers/callbacks.ex new file mode 100644 index 000000000..8e8e5c810 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/callbacks.ex @@ -0,0 +1,150 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Callbacks do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.State + alias ElixirLS.Utils.Matcher + + @type callback :: %{ + type: :callback, + subtype: :callback | :macrocallback, + name: String.t(), + arity: non_neg_integer, + args: String.t(), + args_list: [String.t()], + origin: String.t(), + summary: String.t(), + spec: String.t(), + metadata: map + } + + @doc """ + A reducer that adds suggestions of callbacks. + """ + def add_callbacks(hint, env, buffer_metadata, context, acc) do + text_before = context.text_before + + %State.Env{protocol: protocol, behaviours: behaviours, scope: scope} = env + + list = + Enum.flat_map(behaviours, fn + mod when is_atom(mod) and (protocol == nil or mod != elem(protocol, 0)) -> + mod_name = inspect(mod) + + if Map.has_key?(buffer_metadata.mods_funs_to_positions, {mod, nil, nil}) do + behaviour_callbacks = + buffer_metadata.specs + |> Enum.filter(fn {{behaviour_mod, _, arity}, %State.SpecInfo{kind: kind}} -> + behaviour_mod == mod and is_integer(arity) and kind in [:callback, :macrocallback] + end) + + for {{_, name, arity}, %State.SpecInfo{} = info} <- behaviour_callbacks, + hint == "" or def_prefix?(hint, List.last(info.specs)) or + Matcher.match?("#{name}", hint) do + def_info = buffer_metadata.mods_funs_to_positions[{env.module, name, arity}] + def_info_meta = if def_info, do: def_info.meta, else: %{} + meta = info.meta |> Map.merge(def_info_meta) + + %{ + type: :callback, + subtype: info.kind, + name: Atom.to_string(name), + arity: arity, + args: Enum.join(List.last(info.args), ", "), + args_list: List.last(info.args), + origin: mod_name, + summary: Introspection.extract_summary_from_docs(info.doc), + spec: List.last(info.specs), + metadata: meta + } + end + else + for %{ + name: name, + arity: arity, + kind: kind, + callback: spec, + signature: signature, + doc: doc, + metadata: metadata + } <- + Introspection.get_callbacks_with_docs(mod), + hint == "" or def_prefix?(hint, spec) or Matcher.match?("#{name}", hint) do + desc = Introspection.extract_summary_from_docs(doc) + + {args, args_list} = + if signature do + match_res = Regex.run(~r/.\(([^\)]*)\)/u, signature) + + unless match_res do + raise "unable to get arguments from #{inspect(signature)}" + end + + [_, args_str] = match_res + + args_list = + args_str + |> String.split(",") + |> Enum.map(&String.trim/1) + + args = + args_str + |> String.replace("\n", " ") + |> String.split(",") + |> Enum.map_join(", ", &String.trim/1) + + {args, args_list} + else + if arity == 0 do + {"", []} + else + args_list = for _ <- 1..arity, do: "term" + {Enum.join(args_list, ", "), args_list} + end + end + + def_info = buffer_metadata.mods_funs_to_positions[{env.module, name, arity}] + def_info_meta = if def_info, do: def_info.meta, else: %{} + meta = metadata |> Map.merge(def_info_meta) + + %{ + type: :callback, + subtype: kind, + name: Atom.to_string(name), + arity: arity, + args: args, + args_list: args_list, + origin: mod_name, + summary: desc, + spec: spec, + metadata: meta + } + end + end + + _ -> + [] + end) + + list = Enum.sort(list) + + cond do + Regex.match?(~r/\s(def|defmacro)\s+([_\p{Ll}\p{Lo}][\p{L}\p{N}_]*[?!]?)?$/u, text_before) -> + {:halt, %{acc | result: list}} + + match?({_f, _a}, scope) -> + {:cont, acc} + + true -> + {:cont, %{acc | result: acc.result ++ list}} + end + end + + defp def_prefix?(hint, spec) do + if String.starts_with?(spec, "@macrocallback") do + String.starts_with?("defmacro", hint) + else + String.starts_with?("def", hint) + end + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/complete_engine.ex b/apps/language_server/lib/language_server/providers/completion/reducers/complete_engine.ex new file mode 100644 index 000000000..5424234a7 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/complete_engine.ex @@ -0,0 +1,137 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.CompleteEngine do + @moduledoc false + + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirLS.Utils.CompletionEngine + alias ElixirLS.LanguageServer.Providers.Completion.Reducer + + @type t() :: CompletionEngine.t() + + @doc """ + A reducer that populates the context with the suggestions provided by + the `ElixirLS.Utils.CompletionEngine` module. + + The suggestions are grouped by type and saved in the context under the + `:complete_engine_suggestions_by_type` key and can be accessed by any reducer + that runs after. + + Available suggestions: + + * Modules + * Functions + * Macros + * Variables + * Module attributes + * Variable fields + + """ + def populate(hint, env, buffer_metadata, context, acc, opts \\ []) do + suggestions = + find_mods_funcs( + hint, + context.cursor_position, + env, + buffer_metadata, + context.text_before, + opts + ) + + suggestions_by_type = Enum.group_by(suggestions, & &1.type) + + {:cont, Reducer.put_context(acc, :complete_engine, suggestions_by_type)} + end + + @doc """ + A reducer that adds suggestions of existing modules. + + Note: requires populate/5. + """ + def add_modules(_hint, _env, _file_metadata, _context, acc) do + add_suggestions(:module, acc) + end + + @doc """ + A reducer that adds suggestions of existing functions. + + Note: requires populate/5. + """ + def add_functions(_hint, _env, _file_metadata, _context, acc) do + add_suggestions(:function, acc) + end + + @doc """ + A reducer that adds suggestions of existing macros. + + Note: requires populate/5. + """ + def add_macros(_hint, _env, _file_metadata, _context, acc) do + add_suggestions(:macro, acc) + end + + @doc """ + A reducer that adds suggestions of variable fields. + + Note: requires populate/5. + """ + def add_fields(_hint, _env, _file_metadata, _context, acc) do + add_suggestions(:field, acc) + end + + @doc """ + A reducer that adds suggestions of existing module attributes. + + Note: requires populate/5. + """ + def add_attributes(_hint, _env, _file_metadata, _context, acc) do + add_suggestions(:attribute, acc) + end + + @doc """ + A reducer that adds suggestions of existing variables. + + Note: requires populate/5. + """ + def add_variables(_hint, _env, _file_metadata, _context, acc) do + add_suggestions(:variable, acc) + end + + defp add_suggestions(type, acc) do + suggestions_by_type = Reducer.get_context(acc, :complete_engine) + list = Map.get(suggestions_by_type, type, []) + {:cont, %{acc | result: acc.result ++ list}} + end + + defp find_mods_funcs( + hint, + cursor_position, + %State.Env{ + module: module + } = env, + %Metadata{} = metadata, + text_before, + opts + ) do + hint = + case Source.get_v12_module_prefix(text_before, module) do + nil -> + hint + + module_string -> + # multi alias syntax detected + # prepend module prefix before running completion + prefix = module_string <> "." + prefix <> hint + end + + hint = + if String.starts_with?(hint, "__MODULE__") do + hint |> String.replace_leading("__MODULE__", inspect(module)) + else + hint + end + + CompletionEngine.complete(hint, env, metadata, cursor_position, opts) + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/docs_snippets.ex b/apps/language_server/lib/language_server/providers/completion/reducers/docs_snippets.ex new file mode 100644 index 000000000..e07edd1be --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/docs_snippets.ex @@ -0,0 +1,43 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.DocsSnippets do + @moduledoc false + + # TODO change/move this + alias ElixirSense.Plugins.Util + alias ElixirLS.Utils.Matcher + + # Format: + # {label, snippet, documentation, priority} + @module_attr_snippets [ + {~s(@doc """"""), ~s(@doc """\n$0\n"""), "Documents a function/macro/callback", 13}, + {"@doc false", "@doc false", "Marks this function/macro/callback as internal", 15}, + {~s(@moduledoc """"""), ~s(@moduledoc """\n$0\n"""), "Documents a module", 13}, + {"@moduledoc false", "@moduledoc false", "Marks this module as internal", 15}, + {~s(@typedoc """"""), ~s(@typedoc """\n$0\n"""), "Documents a type specification", 13}, + {"@typedoc false", "@typedoc false", "Marks this type specification as internal", 15} + ] + + @doc """ + A reducer that adds suggestions for @doc, @moduledoc and @typedoc. + """ + def add_snippets(hint, _env, _metadata, %{at_module_body?: true}, acc) do + list = + for {label, snippet, doc, priority} <- @module_attr_snippets, + Matcher.match?(label, hint) do + %{ + type: :generic, + kind: :snippet, + label: label, + snippet: Util.trim_leading_for_insertion(hint, snippet), + filter_text: String.replace_prefix(label, "@", "") |> String.split(" ") |> List.first(), + detail: "module attribute snippet", + documentation: doc, + priority: priority + } + end + + {:cont, %{acc | result: acc.result ++ Enum.sort(list)}} + end + + def add_snippets(_hint, _env, _metadata, _cursor_context, acc), + do: {:cont, acc} +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/overridable.ex b/apps/language_server/lib/language_server/providers/completion/reducers/overridable.ex new file mode 100644 index 000000000..8c1524999 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/overridable.ex @@ -0,0 +1,88 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Overridable do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.State + alias ElixirLS.Utils.Matcher + + @doc """ + A reducer that adds suggestions of overridable functions. + """ + def add_overridable(_hint, %State.Env{scope: {_f, _a}}, _metadata, _cursor_context, acc), + do: {:cont, acc} + + def add_overridable(hint, env, metadata, _cursor_context, acc) do + %State.Env{protocol: protocol, behaviours: behaviours, module: module} = env + + # overridable behaviour callbacks are returned by Reducers.Callbacks + behaviour_callbacks = + Enum.flat_map(behaviours, fn + mod when is_atom(mod) and (protocol == nil or mod != elem(protocol, 0)) -> + for %{ + name: name, + arity: arity + } <- + Introspection.get_callbacks_with_docs(mod) do + {name, arity} + end + + _ -> + [] + end) + + # no need to care of default args here + # only the max arity version can be overridden + list = + for {{^module, name, arity}, %State.ModFunInfo{overridable: {true, origin}} = info} + when is_integer(arity) <- metadata.mods_funs_to_positions, + def_prefix?(hint, info.type) or Matcher.match?("#{name}", hint), + {name, arity} not in behaviour_callbacks do + spec = + case metadata.specs[{module, name, arity}] do + %State.SpecInfo{specs: specs} -> specs |> Enum.join("\n") + nil -> "" + end + + args_list = + info.params + |> List.last() + |> Enum.with_index() + |> Enum.map(&Introspection.param_to_var/1) + + args = args_list |> Enum.join(", ") + + subtype = + case State.ModFunInfo.get_category(info) do + :function -> :callback + :macro -> :macrocallback + end + + %{ + type: :callback, + subtype: subtype, + name: Atom.to_string(name), + arity: arity, + args: args, + args_list: args_list, + origin: inspect(origin), + summary: Introspection.extract_summary_from_docs(info.doc), + metadata: info.meta, + spec: spec + } + end + + {:cont, %{acc | result: acc.result ++ Enum.sort(list)}} + end + + defp def_prefix?(hint, type) when type in [:defmacro, :defmacrop] do + String.starts_with?("defmacro", hint) + end + + defp def_prefix?(hint, type) when type in [:defguard, :defguardp] do + String.starts_with?("defguard", hint) + end + + defp def_prefix?(hint, type) when type in [:def, :defp, :defdelegate] do + String.starts_with?("def", hint) + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/params.ex b/apps/language_server/lib/language_server/providers/completion/reducers/params.ex new file mode 100644 index 000000000..f5404e0be --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/params.ex @@ -0,0 +1,92 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Params do + @moduledoc false + + alias ElixirSense.Core.Binding + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Core.TypeInfo + alias ElixirLS.Utils.Matcher + + @type param_option :: %{ + type: :param_option, + name: String.t(), + origin: String.t(), + type_spec: String.t(), + doc: String.t(), + expanded_spec: String.t() + } + + @doc """ + A reducer that adds suggestions of keyword list options. + """ + def add_options(hint, env, buffer_metadata, cursor_context, acc) do + prefix = cursor_context.text_before + + %State.Env{ + imports: imports, + requires: requires, + aliases: aliases, + module: module, + scope: scope + } = env + + binding_env = Binding.from_env(env, buffer_metadata) + + %Metadata{mods_funs_to_positions: mods_funs, types: metadata_types} = buffer_metadata + + with %{ + candidate: {mod, fun}, + elixir_prefix: elixir_prefix, + npar: npar + } <- + Source.which_func(prefix, binding_env), + {mod, fun, true, :mod_fun} <- + Introspection.actual_mod_fun( + {mod, fun}, + imports, + requires, + if(elixir_prefix, do: [], else: aliases), + module, + scope, + mods_funs, + metadata_types, + {1, 1} + ) do + list = + if Code.ensure_loaded?(mod) do + TypeInfo.extract_param_options(mod, fun, npar) + |> Kernel.++(TypeInfo.extract_param_options(mod, :"MACRO-#{fun}", npar + 1)) + |> options_to_suggestions(mod) + |> Enum.filter(&Matcher.match?(&1.name, hint)) + else + # TODO metadata? + [] + end + + {:cont, %{acc | result: acc.result ++ list}} + else + _ -> + {:cont, acc} + end + end + + defp options_to_suggestions(options, original_module) do + Enum.map(options, fn + {mod, name, type} -> + TypeInfo.get_type_info(mod, type, original_module) + |> Map.merge(%{type: :param_option, name: name |> Atom.to_string()}) + + {mod, name} -> + %{ + doc: "", + expanded_spec: "", + name: name |> Atom.to_string(), + origin: inspect(mod), + type: :param_option, + type_spec: "" + } + end) + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/protocol.ex b/apps/language_server/lib/language_server/providers/completion/reducers/protocol.ex new file mode 100644 index 000000000..d56576623 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/protocol.ex @@ -0,0 +1,80 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Protocol do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.State + alias ElixirLS.Utils.Matcher + + @type protocol_function :: %{ + type: :protocol_function, + name: String.t(), + arity: non_neg_integer, + args: String.t(), + args_list: [String.t()], + origin: String.t(), + summary: String.t(), + spec: String.t(), + metadata: map + } + + @doc """ + A reducer that adds suggestions of protocol functions. + """ + def add_functions(_hint, %State.Env{scope: {_f, _a}}, _metadata, _cursor_context, acc), + do: {:cont, acc} + + def add_functions(_hint, %State.Env{protocol: nil}, _metadata, _cursor_context, acc), + do: {:cont, acc} + + def add_functions(hint, env, buffer_metadata, _cursor_context, acc) do + %State.Env{protocol: {protocol, _implementations}} = env + + mod_name = inspect(protocol) + + list = + if Map.has_key?(buffer_metadata.mods_funs_to_positions, {protocol, nil, nil}) do + behaviour_callbacks = + buffer_metadata.specs + |> Enum.filter(fn {{mod, _, arity}, %State.SpecInfo{kind: kind}} -> + mod == protocol and is_integer(arity) and kind in [:callback] + end) + + for {{_, name, arity}, %State.SpecInfo{} = info} <- behaviour_callbacks, + hint == "" or String.starts_with?("def", hint) or Matcher.match?("#{name}", hint) do + %State.ModFunInfo{} = + def_info = + buffer_metadata.mods_funs_to_positions |> Map.fetch!({protocol, name, arity}) + + %{ + type: :protocol_function, + name: Atom.to_string(name), + arity: arity, + args: Enum.join(List.last(info.args), ", "), + args_list: List.last(info.args), + origin: mod_name, + summary: Introspection.extract_summary_from_docs(def_info.doc), + spec: List.last(info.specs), + metadata: def_info.meta + } + end + else + for {{name, arity}, {_type, args, docs, metadata, spec}} <- + Introspection.module_functions_info(protocol), + hint == "" or String.starts_with?("def", hint) or Matcher.match?("#{name}", hint) do + %{ + type: :protocol_function, + name: Atom.to_string(name), + arity: arity, + args: args |> Enum.join(", "), + args_list: args, + origin: inspect(protocol), + summary: docs, + metadata: metadata, + spec: spec + } + end + end + + {:cont, %{acc | result: acc.result ++ Enum.sort(list)}} + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/returns.ex b/apps/language_server/lib/language_server/providers/completion/reducers/returns.ex new file mode 100644 index 000000000..f5c09d7cc --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/returns.ex @@ -0,0 +1,97 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Returns do + @moduledoc false + + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.State + + @type return :: %{ + type: :return, + description: String.t(), + spec: String.t(), + snippet: String.t() + } + + @doc """ + A reducer that adds suggestions of possible return values. + """ + def add_returns( + "" = _hint, + %State.Env{scope: {fun, arity}} = env, + buffer_metadata, + _context, + acc + ) do + %State.Env{module: current_module, behaviours: behaviours, protocol: protocol} = env + %Metadata{specs: specs} = buffer_metadata + + spec_returns = + case specs[{current_module, fun, arity}] do + nil -> + [] + + %State.SpecInfo{specs: info_specs} -> + for spec <- info_specs, + {:ok, {:@, _, [{_, _, [quoted]}]}} <- [Code.string_to_quoted(spec)], + return <- Introspection.get_returns_from_spec_ast(quoted) do + format_return(return) + end + end + + callbacks = + for mod <- behaviours, + protocol == nil or mod != elem(protocol, 0) do + case specs[{mod, fun, arity}] do + nil -> + for return <- Introspection.get_returns_from_callback(mod, fun, arity) do + format_return(return) + end + + %State.SpecInfo{specs: info_specs} -> + for spec <- info_specs, + {:ok, {:@, _, [{_, _, [quoted]}]}} <- [Code.string_to_quoted(spec)], + return <- Introspection.get_returns_from_spec_ast(quoted) do + format_return(return) + end + end + end + |> List.flatten() + + protocol_functions = + case protocol do + {proto, _implementations} -> + case specs[{proto, fun, arity}] do + nil -> + for return <- Introspection.get_returns_from_callback(proto, fun, arity) do + format_return(return) + end + + %State.SpecInfo{specs: info_specs} -> + for spec <- info_specs, + {:ok, {:@, _, [{:callback, _, [quoted]}]}} <- [Code.string_to_quoted(spec)], + return <- Introspection.get_returns_from_spec_ast(quoted) do + format_return(return) + end + end + + nil -> + [] + end + + list = callbacks ++ protocol_functions ++ spec_returns + {:cont, %{acc | result: acc.result ++ list}} + end + + def add_returns(_hint, _env, _buffer_metadata, _context, acc) do + {:cont, acc} + end + + defp format_return(return) do + %{ + type: :return, + description: return.description, + spec: return.spec, + snippet: return.snippet + } + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/struct.ex b/apps/language_server/lib/language_server/providers/completion/reducers/struct.ex new file mode 100644 index 000000000..2203a0f97 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/struct.ex @@ -0,0 +1,155 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Struct do + @moduledoc false + + alias ElixirSense.Core.Binding + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirLS.Utils.Matcher + + @type field :: %{ + type: :field, + subtype: :struct_field | :map_key, + name: String.t(), + origin: String.t() | nil, + call?: boolean, + type_spec: String.t() | nil + } + + @doc """ + A reducer that adds suggestions of struct fields. + """ + def add_fields(hint, env, buffer_metadata, context, acc) do + text_before = context.text_before + + case find_struct_fields(hint, text_before, env, buffer_metadata) do + {[], _} -> + {:cont, acc} + + {fields, nil} -> + {:halt, %{acc | result: fields}} + + {fields, :maybe_struct_update} -> + reducers = [ + :populate_complete_engine, + :modules, + :functions, + :macros, + :variables, + :attributes + ] + + {:cont, %{acc | result: fields, reducers: reducers}} + end + end + + defp find_struct_fields(hint, text_before, env, buffer_metadata) do + %State.Env{ + module: module, + vars: vars, + attributes: attributes, + imports: imports, + aliases: aliases + } = env + + %Metadata{ + structs: structs, + mods_funs_to_positions: mods_funs, + types: metadata_types, + specs: specs + } = buffer_metadata + + env = %ElixirSense.Core.Binding{ + attributes: attributes, + variables: vars, + structs: structs, + imports: imports, + current_module: module, + specs: specs, + types: metadata_types, + mods_funs: mods_funs + } + + case Source.which_struct(text_before, module) do + {type, fields_so_far, elixir_prefix, var} -> + type = + case {type, elixir_prefix} do + {{:atom, mod}, false} -> + # which_struct returns not expanded aliases + {:atom, Introspection.expand_alias(mod, aliases)} + + _ -> + type + end + + type = Binding.expand(env, {:struct, [], type, var}) + + result = get_fields(buffer_metadata, type, hint, fields_so_far) + {result, if(fields_so_far == [], do: :maybe_struct_update)} + + {:map, fields_so_far, var} -> + var = Binding.expand(env, var) + + result = get_fields(buffer_metadata, var, hint, fields_so_far) + {result, if(fields_so_far == [], do: :maybe_struct_update)} + + _ -> + {[], nil} + end + end + + defp get_fields(metadata, {:map, fields, _}, hint, fields_so_far) do + expand_map_field_access(metadata, fields, hint, :map, fields_so_far) + end + + defp get_fields(metadata, {:struct, fields, type, _}, hint, fields_so_far) do + expand_map_field_access(metadata, fields, hint, {:struct, type}, fields_so_far) + end + + defp get_fields(_, _, _hint, _fields_so_far), do: [] + + defp expand_map_field_access(metadata, fields, hint, type, fields_so_far) do + {subtype, origin, types} = + case type do + {:struct, {:atom, mod}} -> + types = ElixirLS.Utils.Field.get_field_types(metadata, mod, true) + + {:struct_field, inspect(mod), types} + + {:struct, nil} -> + {:struct_field, nil, %{}} + + :map -> + {:map_key, nil, %{}} + end + + for {key, _value} when is_atom(key) <- fields, + key not in fields_so_far, + key_str = Atom.to_string(key), + Matcher.match?(key_str, hint) do + spec = + case types[key] do + nil -> + case key do + :__struct__ -> origin || "atom()" + :__exception__ -> "true" + _ -> nil + end + + some -> + Introspection.to_string_with_parens(some) + end + + %{ + type: :field, + name: key_str, + subtype: subtype, + origin: origin, + call?: false, + type_spec: spec + } + end + |> Enum.sort_by(& &1.name) + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/type_specs.ex b/apps/language_server/lib/language_server/providers/completion/reducers/type_specs.ex new file mode 100644 index 000000000..e9cb65c7a --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/reducers/type_specs.ex @@ -0,0 +1,183 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.TypeSpecs do + @moduledoc false + + alias ElixirSense.Core.Binding + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Core.TypeInfo + alias ElixirLS.Utils.Matcher + + @type type_spec :: %{ + type: :type_spec, + name: String.t(), + arity: non_neg_integer, + origin: String.t() | nil, + args_list: list(String.t()), + spec: String.t(), + doc: String.t(), + signature: String.t(), + metadata: map + } + + @doc """ + A reducer that adds suggestions of type specs. + """ + + # We only list type specs when inside typespec scope + def add_types(hint, env, file_metadata, %{at_module_body?: _}, acc) do + if match?({:typespec, _, _}, env.scope) do + %State.Env{ + aliases: aliases, + module: module, + scope: scope + } = env + + %Metadata{mods_funs_to_positions: mods_funs, types: metadata_types} = file_metadata + + binding_env = Binding.from_env(env, file_metadata) + + {mod, hint} = + hint + |> Source.split_module_and_hint(module, aliases) + |> expand(binding_env, aliases) + + list = + find_typespecs_for_mod_and_hint( + {mod, hint}, + aliases, + module, + scope, + mods_funs, + metadata_types + ) + |> Kernel.++(find_builtin_types({mod, hint})) + + {:cont, %{acc | result: acc.result ++ list}} + else + {:cont, acc} + end + end + + def add_types(_hint, _env, _buffer_metadata, _context, acc) do + {:cont, acc} + end + + defp expand({{kind, _} = type, hint}, env, aliases) when kind in [:attribute, :variable] do + case Binding.expand(env, type) do + {:atom, module} -> {Introspection.expand_alias(module, aliases), hint} + _ -> {nil, ""} + end + end + + defp expand({type, hint}, _env, _aliases) do + {type, hint} + end + + defp find_typespecs_for_mod_and_hint( + {mod, hint}, + aliases, + module, + scope, + mods_funs, + metadata_types + ) do + case Introspection.actual_module(mod, aliases, module, scope, mods_funs) do + {actual_mod, true} -> + find_module_types(actual_mod, {mod, hint}, metadata_types, module) + + {nil, false} -> + find_module_types(module, {mod, hint}, metadata_types, module) + + {_, false} -> + [] + end + end + + defp find_builtin_types({nil, hint}) do + TypeInfo.find_all_builtin(&Matcher.match?("#{&1.name}", hint)) + |> Enum.map(&type_info_to_suggestion(&1, nil)) + |> Enum.sort_by(fn %{name: name, arity: arity} -> {name, arity} end) + end + + defp find_builtin_types({_mod, _hint}), do: [] + + defp find_module_types(actual_mod, {mod, hint}, metadata_types, module) do + find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) + |> Kernel.++(TypeInfo.find_all(actual_mod, &Matcher.match?("#{&1.name}", hint))) + |> Enum.map(&type_info_to_suggestion(&1, actual_mod)) + |> Enum.uniq_by(fn %{name: name, arity: arity} -> {name, arity} end) + |> Enum.sort_by(fn %{name: name, arity: arity} -> {name, arity} end) + end + + defp find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) do + # local types are hoisted, no need to check position + include_private = mod == nil and actual_mod == module + + for {{^actual_mod, type, arity}, type_info} when is_integer(arity) <- metadata_types, + type |> Atom.to_string() |> Matcher.match?(hint), + include_private or type_info.kind != :typep, + do: type_info + end + + defp type_info_to_suggestion(type_info, module) do + origin = if module, do: inspect(module) + + case type_info do + %ElixirSense.Core.State.TypeInfo{args: [args]} -> + args_stringified = Enum.join(args, ", ") + + spec = + case type_info.kind do + :opaque -> "@opaque #{type_info.name}(#{args_stringified})" + _ -> List.last(type_info.specs) + end + + %{ + type: :type_spec, + name: type_info.name |> Atom.to_string(), + arity: length(args), + args_list: args, + signature: "#{type_info.name}(#{args_stringified})", + origin: origin, + doc: Introspection.extract_summary_from_docs(type_info.doc), + spec: spec, + metadata: type_info.meta + } + + _ -> + args_list = + if type_info.signature do + part = + type_info.signature + |> String.split("(") + |> Enum.at(1) + + if part do + part + |> String.split(")") + |> Enum.at(0) + |> String.split(",") + |> Enum.map(&String.trim/1) + else + [] + end + else + [] + end + + %{ + type: :type_spec, + name: type_info.name |> Atom.to_string(), + arity: type_info.arity, + args_list: args_list, + signature: type_info.signature, + origin: origin, + doc: type_info.doc, + spec: type_info.spec, + metadata: type_info.metadata + } + end + end +end diff --git a/apps/language_server/lib/language_server/providers/completion/suggestion.ex b/apps/language_server/lib/language_server/providers/completion/suggestion.ex new file mode 100644 index 000000000..6081e833e --- /dev/null +++ b/apps/language_server/lib/language_server/providers/completion/suggestion.ex @@ -0,0 +1,277 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.Suggestion do + @moduledoc """ + Provider responsible for finding suggestions for auto-completing. + + It provides suggestions based on a list of pre-defined reducers. + + ## Reducers + + A reducer is a function with the following spec: + + @spec reducer( + String.t(), + String.t(), + State.Env.t(), + Metadata.t(), + acc() + ) :: {:cont | :halt, acc()} + + ## Examples + + Adding suggestions: + + def my_reducer(hint, prefix, env, buffer_metadata, acc) do + suggestions = ... + {:cont, %{acc | result: acc.result ++ suggestions}} + end + + Defining the only set of suggestions to be provided: + + def my_reducer(hint, prefix, env, buffer_metadata, acc) do + suggestions = ... + {:halt, %{acc | result: suggestions}} + end + + Defining a list of suggestions to be provided and allow an extra + limited set of additional reducers to run next: + + def my_reducer(hint, prefix, env, buffer_metadata, acc) do + suggestions = ... + {:cont, %{acc | result: fields, reducers: [:populate_complete_engine, :variables]}} + end + """ + + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.ModuleStore + alias ElixirSense.Core.State + alias ElixirSense.Core.Parser + alias ElixirSense.Core.Source + alias ElixirLS.LanguageServer.Providers.Completion.Reducers + + @type generic :: %{ + type: :generic, + label: String.t(), + detail: String.t() | nil, + documentation: String.t() | nil, + insert_text: String.t() | nil, + filter_text: String.t() | nil, + snippet: String.t() | nil, + priority: integer() | nil, + kind: atom(), + command: map() + } + + @type suggestion :: + generic() + | Reducers.CompleteEngine.t() + | Reducers.Struct.field() + | Reducers.Returns.return() + | Reducers.Callbacks.callback() + | Reducers.Protocol.protocol_function() + | Reducers.Params.param_option() + | Reducers.TypeSpecs.type_spec() + | Reducers.Bitstring.bitstring_option() + + @type acc :: %{result: [suggestion], reducers: [atom], context: map} + @type cursor_context :: %{ + text_before: String.t(), + text_after: String.t(), + at_module_body?: boolean(), + cursor_position: {pos_integer, pos_integer} + } + + @reducers [ + structs_fields: &Reducers.Struct.add_fields/5, + returns: &Reducers.Returns.add_returns/5, + callbacks: &Reducers.Callbacks.add_callbacks/5, + protocol_functions: &Reducers.Protocol.add_functions/5, + overridable: &Reducers.Overridable.add_overridable/5, + param_options: &Reducers.Params.add_options/5, + typespecs: &Reducers.TypeSpecs.add_types/5, + populate_complete_engine: &Reducers.CompleteEngine.populate/6, + variables: &Reducers.CompleteEngine.add_variables/5, + modules: &Reducers.CompleteEngine.add_modules/5, + functions: &Reducers.CompleteEngine.add_functions/5, + macros: &Reducers.CompleteEngine.add_macros/5, + variable_fields: &Reducers.CompleteEngine.add_fields/5, + attributes: &Reducers.CompleteEngine.add_attributes/5, + docs_snippets: &Reducers.DocsSnippets.add_snippets/5, + bitstring_options: &Reducers.Bitstring.add_bitstring_options/5 + ] + + @add_opts_for [:populate_complete_engine] + + @spec suggestions(String.t(), pos_integer, pos_integer, keyword()) :: [Suggestion.suggestion()] + def suggestions(code, line, column, options \\ []) do + hint = Source.prefix(code, line, column) + + metadata = + Keyword.get_lazy(options, :metadata, fn -> + Parser.parse_string(code, true, true, {line, column}) + end) + + {text_before, text_after} = Source.split_at(code, line, column) + + metadata = + maybe_fix_autocomple_on_cursor( + metadata, + text_before, + text_after, + {line, column} + ) + + env = + Metadata.get_env(metadata, {line, column}) + |> Metadata.add_scope_vars( + metadata, + {line, column}, + &(to_string(&1.name) != hint) + ) + + # if variable is rebound then in env there are many variables with the same name + # find the one defined closest to cursor + vars = + env.vars + |> Enum.group_by(fn %State.VarInfo{name: name} -> name end) + |> Enum.map(fn {_name, list} -> + list + |> Enum.max_by(fn + %State.VarInfo{positions: [_position]} -> + # variable is being defined - it's not a good candidate + {0, 0} + + %State.VarInfo{positions: positions} -> + Enum.min(positions) + end) + end) + + env = %{env | vars: vars} + + module_store = ModuleStore.build() + + cursor_context = %{ + cursor_position: {line, column}, + text_before: text_before, + text_after: text_after, + at_module_body?: Metadata.at_module_body?(env) + } + + find(hint, env, metadata, cursor_context, module_store, options) + end + + # Provides a last attempt to fix a couple of parse errors that + # commonly appear when working with autocomplete on functions + # without parens. + # + # Note: This will be removed after refactoring the parser to + # allow unparsable nodes in the AST. + defp maybe_fix_autocomple_on_cursor(%Metadata{error: nil} = meta, _, _, _) do + meta + end + + defp maybe_fix_autocomple_on_cursor(metadata, text_before, text_after, {line, column}) do + # Fix incomplete call, e.g. cursor after `var.` + fix_incomplete_call = fn text_before, text_after -> + if String.ends_with?(text_before, ".") do + text_before <> "__fake_call__" <> text_after + end + end + + # Fix incomplete kw, e.g. cursor after `option1: 1,` + fix_incomplete_kw = fn text_before, text_after -> + if Regex.match?(~r/\,\s*$/u, text_before) do + text_before <> "__fake_key__: :__fake_value__" <> text_after + end + end + + # Fix incomplete kw key, e.g. cursor after `option1: 1, opt` + fix_incomplete_kw_key = fn text_before, text_after -> + if Regex.match?(~r/\,\s*([\p{L}_][\p{L}\p{N}_@]*[?!]?)?$/u, text_before) do + text_before <> ": :__fake_value__" <> text_after + end + end + + # TODO this may no longer be needed + # only fix_incomplete_call has some tests depending on it + fixers = [ + fix_incomplete_call, + fix_incomplete_kw, + fix_incomplete_kw_key + ] + + Enum.reduce_while(fixers, nil, fn fun, _ -> + new_buffer = fun.(text_before, text_after) + + with true <- new_buffer != nil, + meta <- Parser.parse_string(new_buffer, false, true, {line, column}), + %Metadata{error: error} <- meta, + true <- error == nil do + {:halt, meta} + else + _ -> + {:cont, metadata} + end + end) + end + + @doc """ + Finds all suggestions for a hint based on context information. + """ + @spec find(String.t(), State.Env.t(), Metadata.t(), cursor_context, ModuleStore.t(), keyword()) :: + [suggestion()] + def find( + hint, + env, + buffer_metadata, + cursor_context, + %{plugins: plugins} = module_store, + opts \\ [] + ) do + reducers = + plugins + |> Enum.filter(&function_exported?(&1, :reduce, 5)) + |> Enum.map(fn module -> + {module, &module.reduce/5} + end) + |> Enum.concat(@reducers) + |> maybe_add_opts(opts) + + context = + plugins + |> Enum.filter(&function_exported?(&1, :setup, 1)) + |> Enum.reduce(%{module_store: module_store}, fn plugin, context -> + plugin.setup(context) + end) + + acc = %{result: [], reducers: Keyword.keys(reducers), context: context} + + %{result: result} = + Enum.reduce_while(reducers, acc, fn {key, fun}, acc -> + if key in acc.reducers do + fun.(hint, env, buffer_metadata, cursor_context, acc) + else + {:cont, acc} + end + end) + + for item <- result do + plugins + |> Enum.filter(&function_exported?(&1, :decorate, 1)) + |> Enum.reduce(item, fn module, item -> module.decorate(item) end) + end + end + + defp maybe_add_opts(reducers, opts) do + Enum.map(reducers, fn {name, reducer} -> + if name in @add_opts_for do + {name, reducer_with_opts(reducer, opts)} + else + {name, reducer} + end + end) + end + + defp reducer_with_opts(fun, opts) do + fn a, b, c, d, e -> fun.(a, b, c, d, e, opts) end + end +end diff --git a/apps/language_server/lib/language_server/providers/definition.ex b/apps/language_server/lib/language_server/providers/definition.ex index dfaa4a419..25ea03bf1 100644 --- a/apps/language_server/lib/language_server/providers/definition.ex +++ b/apps/language_server/lib/language_server/providers/definition.ex @@ -4,6 +4,7 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do """ alias ElixirLS.LanguageServer.{Protocol, Parser} + alias ElixirLS.LanguageServer.Providers.Definition.Locator def definition( uri, @@ -13,11 +14,11 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do project_dir ) do result = - case ElixirSense.definition(source_file.text, line, character, metadata: metadata) do + case Locator.definition(source_file.text, line, character, metadata: metadata) do nil -> nil - %ElixirSense.Location{} = location -> + %ElixirLS.LanguageServer.Location{} = location -> Protocol.Location.new(location, uri, source_file.text, project_dir) end diff --git a/apps/language_server/lib/language_server/providers/definition/locator.ex b/apps/language_server/lib/language_server/providers/definition/locator.ex new file mode 100644 index 000000000..10491f98e --- /dev/null +++ b/apps/language_server/lib/language_server/providers/definition/locator.ex @@ -0,0 +1,296 @@ +defmodule ElixirLS.LanguageServer.Providers.Definition.Locator do + @moduledoc """ + Provides a function to find out where symbols are defined. + + Currently finds definition of modules, functions and macros, + typespecs, variables and attributes. + """ + + alias ElixirSense.Core.Binding + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.State + alias ElixirSense.Core.State.ModFunInfo + alias ElixirSense.Core.State.TypeInfo + alias ElixirSense.Core.State.VarInfo + alias ElixirSense.Core.Source + alias ElixirSense.Core.SurroundContext + alias ElixirLS.LanguageServer.Location + alias ElixirSense.Core.Parser + # TODO change/move this + alias ElixirSense.Plugins.Phoenix.Scope + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode + + def definition(code, line, column, options \\ []) do + case NormalizedCode.Fragment.surround_context(code, {line, column}) do + :none -> + nil + + context -> + metadata = + Keyword.get_lazy(options, :metadata, fn -> + Parser.parse_string(code, true, true, {line, column}) + end) + + env = + Metadata.get_env(metadata, {line, column}) + |> Metadata.add_scope_vars(metadata, {line, column}) + + find( + context, + env, + metadata + ) + end + end + + @doc """ + Finds out where a module, function, macro or variable was defined. + """ + @spec find( + any(), + State.Env.t(), + Metadata.t() + ) :: %Location{} | nil + def find( + context, + %State.Env{ + module: module, + vars: vars, + attributes: attributes + } = env, + metadata + ) do + binding_env = Binding.from_env(env, metadata) + + type = SurroundContext.to_binding(context.context, module) + + case type do + nil -> + nil + + {:keyword, _} -> + nil + + {:variable, variable} -> + var_info = + vars + |> Enum.find(fn + %VarInfo{name: name, positions: positions} -> + name == variable and context.begin in positions + end) + + if var_info != nil do + {definition_line, definition_column} = Enum.min(var_info.positions) + + %Location{type: :variable, file: nil, line: definition_line, column: definition_column} + else + find_function_or_module( + {nil, variable}, + context, + env, + metadata, + binding_env + ) + end + + {:attribute, attribute} -> + attribute_info = + Enum.find(attributes, fn + %State.AttributeInfo{name: name} -> name == attribute + end) + + if attribute_info != nil do + %State.AttributeInfo{positions: [{line, column} | _]} = attribute_info + %Location{type: :attribute, file: nil, line: line, column: column} + end + + {module, function} -> + find_function_or_module( + {module, function}, + context, + env, + metadata, + binding_env + ) + end + end + + defp find_function_or_module( + target, + context, + env, + metadata, + binding_env, + visited \\ [] + ) do + unless target in visited do + do_find_function_or_module( + target, + context, + env, + metadata, + binding_env, + [target | visited] + ) + end + end + + defp do_find_function_or_module( + {{kind, _} = type, function}, + context, + env, + metadata, + binding_env, + visited + ) + when kind in [:attribute, :variable] do + case Binding.expand(binding_env, type) do + {:atom, module} -> + do_find_function_or_module( + {{:atom, Introspection.expand_alias(module, env.aliases)}, function}, + context, + env, + metadata, + binding_env, + visited + ) + + _ -> + nil + end + end + + defp do_find_function_or_module( + {nil, :super}, + context, + %State.Env{scope: {function, arity}, module: module} = env, + metadata, + binding_env, + visited + ) do + case metadata.mods_funs_to_positions[{module, function, arity}] do + %ModFunInfo{overridable: {true, origin}} -> + # overridable function is most likely defined by __using__ macro + do_find_function_or_module( + {{:atom, origin}, :__using__}, + context, + env, + metadata, + binding_env, + visited + ) + + _ -> + nil + end + end + + defp do_find_function_or_module( + {module, function}, + context, + env, + metadata, + _binding_env, + _visited + ) do + %State.Env{ + module: current_module, + imports: imports, + requires: requires, + aliases: aliases, + scope: scope + } = env + + m = get_module(module, context, env, metadata) + + case {m, function} + |> Introspection.actual_mod_fun( + imports, + requires, + aliases, + current_module, + scope, + metadata.mods_funs_to_positions, + metadata.types, + context.begin + ) do + {_, _, false, _} -> + nil + + {mod, fun, true, :mod_fun} -> + {line, column} = context.end + call_arity = Metadata.get_call_arity(metadata, mod, fun, line, column) || :any + + fn_definition = + Location.get_function_position_using_metadata( + mod, + fun, + call_arity, + metadata.mods_funs_to_positions + ) + + case fn_definition do + nil -> + Location.find_mod_fun_source(mod, fun, call_arity) + + %ModFunInfo{positions: positions} = mi -> + # for simplicity take last position here as positions are reversed + {line, column} = positions |> Enum.at(-1) + + %Location{ + file: nil, + type: ModFunInfo.get_category(mi), + line: line, + column: column + } + end + + {mod, fun, true, :type} -> + {line, column} = context.end + call_arity = Metadata.get_call_arity(metadata, mod, fun, line, column) || :any + + type_definition = + Location.get_type_position_using_metadata(mod, fun, call_arity, metadata.types) + + case type_definition do + nil -> + Location.find_type_source(mod, fun, call_arity) + + %TypeInfo{positions: positions} -> + # for simplicity take last position here as positions are reversed + {line, column} = positions |> Enum.at(-1) + + %Location{ + file: nil, + type: :typespec, + line: line, + column: column + } + end + end + end + + defp get_module(module, %{end: {line, col}}, env, metadata) do + with {true, module} <- get_phoenix_module(module, env), + true <- Introspection.elixir_module?(module) do + text_before = Source.text_before(metadata.source, line, col) + + case Scope.within_scope(text_before) do + {false, _} -> + module + + {true, scope_alias} -> + Module.concat(scope_alias, module) + end + end + end + + defp get_phoenix_module(module, env) do + case {Phoenix.Router in env.requires, module} do + {true, {:atom, module}} -> {true, module} + {false, {:atom, module}} -> module + _ -> nil + end + end +end diff --git a/apps/language_server/lib/language_server/providers/execute_command/expand_macro.ex b/apps/language_server/lib/language_server/providers/execute_command/expand_macro.ex index 5a4ad670b..646c9a422 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/expand_macro.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/expand_macro.ex @@ -5,6 +5,11 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ExpandMacro do """ alias ElixirLS.LanguageServer.Server + alias ElixirSense.Core.Ast + alias ElixirSense.Core.MacroExpander + alias ElixirSense.Core.State + alias ElixirSense.Core.Parser + alias ElixirSense.Core.Metadata @behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand @@ -14,9 +19,10 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ExpandMacro do source_file = Server.get_source_file(state, uri) cur_text = source_file.text + # TODO change/move this if String.trim(text) != "" do formatted = - ElixirSense.expand_full(cur_text, text, line + 1) + expand_full(cur_text, text, line + 1) |> Map.new(fn {key, value} -> key = key @@ -44,4 +50,44 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ExpandMacro do }} end end + + def expand_full(buffer, code, line) do + buffer_file_metadata = Parser.parse_string(buffer, true, true, {line, 1}) + + env = Metadata.get_env(buffer_file_metadata, {line, 1}) + + do_expand_full(code, env) + end + + def do_expand_full(code, %State.Env{requires: requires, imports: imports, module: module}) do + env = + %Macro.Env{macros: __ENV__.macros} + |> Ast.set_module_for_env(module) + |> Ast.add_requires_to_env(requires) + |> Ast.add_imports_to_env(imports) + + try do + {:ok, expr} = code |> Code.string_to_quoted() + + # Elixir require some meta to expand ast + expr = MacroExpander.add_default_meta(expr) + + %{ + expand_once: expr |> Macro.expand_once(env) |> Macro.to_string(), + expand: expr |> Macro.expand(env) |> Macro.to_string(), + expand_partial: expr |> Ast.expand_partial(env) |> Macro.to_string(), + expand_all: expr |> Ast.expand_all(env) |> Macro.to_string() + } + rescue + e -> + message = inspect(e) + + %{ + expand_once: message, + expand: message, + expand_partial: message, + expand_all: message + } + end + end end diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index 8df854912..54afeb0c3 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -2,6 +2,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do alias ElixirLS.LanguageServer.{SourceFile, DocLinks, Parser} import ElixirLS.LanguageServer.Protocol alias ElixirLS.LanguageServer.MarkdownUtils + alias ElixirLS.LanguageServer.Providers.Hover.Docs require Logger @moduledoc """ @@ -10,29 +11,17 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do def hover(%Parser.Context{source_file: source_file, metadata: metadata}, line, character) do response = - case ElixirSense.docs(source_file.text, line, character, metadata: metadata) do + case Docs.docs(source_file.text, line, character, metadata: metadata) do nil -> nil %{docs: docs, range: es_range} -> lines = SourceFile.lines(source_file.text) - try do - %{ - "contents" => contents(docs), - "range" => build_range(lines, es_range) - } - rescue - e -> - if match?({_, _}, docs) do - Logger.error("Sanity check failed. ElixirLS needs to restart.") - - Process.sleep(2000) - System.halt(1) - end - - raise "#{inspect(e.__struct__)}\n#{inspect(__STACKTRACE__)}\nline:\n#{Enum.at(lines, line - 1)}\nchar: #{character}\n#{inspect(docs)}" - end + %{ + "contents" => contents(docs), + "range" => build_range(lines, es_range) + } end {:ok, response} diff --git a/apps/language_server/lib/language_server/providers/hover/docs.ex b/apps/language_server/lib/language_server/providers/hover/docs.ex new file mode 100644 index 000000000..89761b85f --- /dev/null +++ b/apps/language_server/lib/language_server/providers/hover/docs.ex @@ -0,0 +1,679 @@ +defmodule ElixirLS.LanguageServer.Providers.Hover.Docs do + alias ElixirSense.Core.Binding + alias ElixirSense.Core.BuiltinAttributes + alias ElixirSense.Core.BuiltinFunctions + alias ElixirSense.Core.BuiltinTypes + require ElixirSense.Core.Introspection, as: Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode + alias ElixirSense.Core.Normalized.Typespec + alias ElixirSense.Core.ReservedWords + alias ElixirSense.Core.State + alias ElixirSense.Core.SurroundContext + alias ElixirSense.Core.State.ModFunInfo + alias ElixirSense.Core.State.VarInfo + alias ElixirSense.Core.TypeInfo + alias ElixirSense.Core.Parser + + @type markdown :: String.t() + + @type module_doc :: %{kind: :module, docs: markdown, metadata: map, module: module()} + + @type function_doc :: %{ + kind: :function | :macro, + module: module(), + function: atom(), + arity: non_neg_integer(), + args: list(String.t()), + metadata: map(), + specs: list(String.t()), + docs: markdown() + } + + @type type_doc :: %{ + kind: :type, + module: module() | nil, + type: atom(), + arity: non_neg_integer(), + args: list(String.t()), + metadata: map(), + spec: String.t(), + docs: markdown() + } + + @type variable_doc :: %{ + name: atom(), + kind: :variable + } + + @type attribute_doc :: %{ + name: atom(), + kind: :attribute, + docs: markdown() + } + + @type keyword_doc :: %{ + name: atom(), + kind: :attribute, + docs: markdown() + } + + @type doc :: module_doc | function_doc | type_doc | variable_doc | attribute_doc | keyword_doc + + @builtin_functions BuiltinFunctions.all() + |> Enum.map(&elem(&1, 0)) + |> Kernel.--([:exception, :message]) + + def docs(code, line, column, options \\ []) do + case NormalizedCode.Fragment.surround_context(code, {line, column}) do + :none -> + nil + + %{begin: begin_pos, end: end_pos} = context -> + metadata = + Keyword.get_lazy(options, :metadata, fn -> + Parser.parse_string(code, true, true, {line, column}) + end) + + env = Metadata.get_env(metadata, {line, column}) + + case all(context, env, metadata) do + [] -> + nil + + list -> + %{ + docs: list, + range: %{ + begin: begin_pos, + end: end_pos + } + } + end + end + end + + defp all( + context, + %State.Env{ + module: module, + vars: vars + } = env, + metadata + ) do + binding_env = Binding.from_env(env, metadata) + + type = SurroundContext.to_binding(context.context, module) + + case type do + nil -> + nil + + {:keyword, keyword} -> + docs = ReservedWords.docs(keyword) + + %{ + name: Atom.to_string(keyword), + kind: :keyword, + docs: docs + } + + {:attribute, attribute} -> + docs = BuiltinAttributes.docs(attribute) || "" + + %{ + name: Atom.to_string(attribute), + kind: :attribute, + docs: docs + } + + {:variable, variable} -> + {line, column} = context.begin + + var_info = + vars + |> Enum.find(fn + %VarInfo{name: name, positions: positions} -> + name == variable and {line, column} in positions + end) + + if var_info != nil do + %{ + name: Atom.to_string(variable), + kind: :variable + } + else + mod_fun_docs( + type, + context, + binding_env, + env, + metadata + ) + end + + _ -> + mod_fun_docs( + type, + context, + binding_env, + env, + metadata + ) + end + |> List.wrap() + end + + defp mod_fun_docs( + {mod, fun}, + context, + binding_env, + env, + metadata + ) do + actual = + {Binding.expand(binding_env, mod), fun} + |> expand(env.aliases) + |> Introspection.actual_mod_fun( + env.imports, + env.requires, + env.aliases, + env.module, + env.scope, + metadata.mods_funs_to_positions, + metadata.types, + context.begin + ) + + case actual do + {mod, fun, true, kind} -> + {line, column} = context.end + call_arity = Metadata.get_call_arity(metadata, mod, fun, line, column) || :any + get_all_docs({mod, fun, call_arity}, metadata, env, kind) + + _ -> + nil + end + end + + def get_all_docs({mod, nil, _}, metadata, _env, :mod_fun) do + doc_info = + metadata.mods_funs_to_positions + |> Enum.find_value(fn + {{^mod, nil, nil}, fun_info = %ModFunInfo{}} -> + %{ + kind: :module, + module: mod, + metadata: fun_info.meta, + docs: fun_info.doc + } + + _ -> + false + end) + + if doc_info == nil do + get_module_docs(mod) + else + doc_info + end + end + + def get_all_docs({mod, fun, arity}, metadata, env, :mod_fun) do + doc_infos = + metadata.mods_funs_to_positions + |> Enum.filter(fn + {{^mod, ^fun, a}, fun_info} when not is_nil(a) and fun not in @builtin_functions -> + default_args = fun_info.params |> Enum.at(-1) |> Introspection.count_defaults() + + Introspection.matches_arity_with_defaults?(a, default_args, arity) + + _ -> + false + end) + |> Enum.sort_by(fn {{^mod, ^fun, a}, _fun_info} -> a end) + |> Enum.map(fn {{^mod, ^fun, a}, fun_info} -> + kind = ModFunInfo.get_category(fun_info) + + fun_args_text = + fun_info.params + |> List.last() + |> Enum.with_index() + |> Enum.map(&(&1 |> Introspection.param_to_var())) + + specs = + case metadata.specs[{mod, fun, a}] do + nil -> + [] + + %State.SpecInfo{specs: specs} -> + specs |> Enum.reverse() + end + + meta = fun_info.meta + + behaviour_implementation = + Metadata.get_module_behaviours(metadata, env, mod) + |> Enum.find_value(fn behaviour -> + if Introspection.is_callback(behaviour, fun, a, metadata) do + behaviour + end + end) + + case behaviour_implementation do + nil -> + %{ + kind: kind, + module: mod, + function: fun, + arity: a, + args: fun_args_text, + metadata: meta, + specs: specs, + docs: fun_info.doc + } + + behaviour -> + meta = Map.merge(meta, %{implementing: behaviour}) + + case metadata.specs[{behaviour, fun, a}] do + %State.SpecInfo{} = spec_info -> + specs = + spec_info.specs + |> Enum.reject(&String.starts_with?(&1, "@spec")) + |> Enum.reverse() + + {callback_doc, callback_meta} = + case metadata.mods_funs_to_positions[{behaviour, fun, a}] do + nil -> + {spec_info.doc, spec_info.meta} + + def_info -> + # in case of protocol implementation get doc and meta from def + {def_info.doc, def_info.meta} + end + + %{ + kind: kind, + module: mod, + function: fun, + arity: a, + args: fun_args_text, + metadata: callback_meta |> Map.merge(meta), + specs: specs, + docs: callback_doc + } + + nil -> + callback_docs_entry = + NormalizedCode.callback_documentation(behaviour) + |> Enum.find_value(fn + {{^fun, ^a}, doc} -> doc + _ -> false + end) + + case callback_docs_entry do + nil -> + # pass meta with implementing flag to trigger looking for specs in behaviour module + # assume there is a typespec for behaviour module + specs = [ + Introspection.get_spec_as_string( + mod, + fun, + a, + State.ModFunInfo.get_category(fun_info), + meta + ) + ] + + %{ + kind: kind, + module: mod, + function: fun, + arity: a, + args: fun_args_text, + metadata: meta, + specs: specs, + docs: "" + } + + {_, docs, callback_meta, mime_type} -> + app = ElixirSense.Core.Applications.get_application(behaviour) + docs = docs |> NormalizedCode.extract_docs(mime_type, behaviour, app) + # as of OTP 25 erlang callback doc entry does not have signature in meta + # pass meta with implementing flag to trigger looking for specs in behaviour module + # assume there is a typespec for behaviour module + specs = [ + Introspection.get_spec_as_string( + mod, + fun, + a, + State.ModFunInfo.get_category(fun_info), + meta + ) + ] + + %{ + kind: kind, + module: mod, + function: fun, + arity: a, + args: fun_args_text, + metadata: callback_meta |> Map.merge(meta), + specs: specs, + docs: docs || "" + } + end + end + end + end) + + if doc_infos == [] do + get_func_docs(mod, fun, arity) + else + doc_infos + end + end + + def get_all_docs({mod, fun, arity}, metadata, _env, :type) do + doc_infos = + metadata.types + |> Enum.filter(fn + {{^mod, ^fun, a}, _type_info} when not is_nil(a) -> + Introspection.matches_arity?(a, arity) + + _ -> + false + end) + |> Enum.sort_by(fn {{_mod, _fun, a}, _type_info} -> a end) + |> Enum.map(fn {{_mod, _fun, a}, type_info} -> + args = type_info.args |> List.last() + + spec = + case type_info.kind do + :opaque -> "@opaque #{fun}(#{args})" + _ -> List.last(type_info.specs) + end + + %{ + kind: :type, + module: mod, + type: fun, + arity: a, + args: args, + metadata: type_info.meta, + spec: spec, + docs: type_info.doc + } + end) + + if doc_infos == [] do + get_type_docs(mod, fun, arity) + else + doc_infos + end + end + + @spec get_module_docs(atom) :: + nil | module_doc() + def get_module_docs(mod) when is_atom(mod) do + case NormalizedCode.get_docs(mod, :moduledoc) do + {_line, text, metadata} -> + %{ + kind: :module, + module: mod, + metadata: metadata, + docs: text || "" + } + + _ -> + if Code.ensure_loaded?(mod) do + app = ElixirSense.Core.Applications.get_application(mod) + + %{ + kind: :module, + module: mod, + metadata: %{app: app}, + docs: "" + } + end + end + end + + # TODO spec + @spec get_func_docs(nil | module, atom, non_neg_integer | :any) :: list(function_doc()) + def get_func_docs(mod, fun, arity) + when mod != nil and fun in @builtin_functions do + for {f, a} <- BuiltinFunctions.all(), f == fun, Introspection.matches_arity?(a, arity) do + spec = BuiltinFunctions.get_specs({f, a}) + args = BuiltinFunctions.get_args({f, a}) + docs = BuiltinFunctions.get_docs({f, a}) + + metadata = %{builtin: true} + + %{ + kind: :function, + module: mod, + function: fun, + arity: a, + args: args, + metadata: metadata, + specs: spec, + docs: docs + } + end + end + + def get_func_docs(mod, fun, call_arity) do + case NormalizedCode.get_docs(mod, :docs) do + nil -> + # no docs, fallback to typespecs + get_func_docs_from_typespec(mod, fun, call_arity) + + docs -> + results = + for {{f, arity}, _, kind, args, text, metadata} <- docs, + f == fun, + Introspection.matches_arity_with_defaults?( + arity, + Map.get(metadata, :defaults, 0), + call_arity + ) do + fun_args_text = + Introspection.get_fun_args_from_doc_or_typespec(mod, f, arity, args, metadata) + + %{ + kind: kind, + module: mod, + function: fun, + arity: arity, + args: fun_args_text, + metadata: metadata, + specs: Introspection.get_specs_text(mod, fun, arity, kind, metadata), + docs: text || "" + } + end + + case results do + [] -> + get_func_docs_from_typespec(mod, fun, call_arity) + + other -> + other + end + end + end + + defp get_func_docs_from_typespec(mod, fun, call_arity) do + # TypeInfo.get_function_specs does fallback to behaviours + {behaviour, specs} = TypeInfo.get_function_specs(mod, fun, call_arity) + app = ElixirSense.Core.Applications.get_application(mod) + + meta = + if behaviour do + %{implementing: behaviour} + else + %{} + end + + results = + for {{_name, arity}, [params | _]} <- specs do + fun_args_text = TypeInfo.extract_params(params) + + %{ + kind: :function, + module: mod, + function: fun, + arity: arity, + args: fun_args_text, + metadata: meta |> Map.put(:app, app), + specs: Introspection.get_specs_text(mod, fun, arity, :function, meta), + docs: "" + } + end + + case results do + [] -> + # no docs and no typespecs + get_func_docs_from_module_info(mod, fun, call_arity) + + other -> + other + end + end + + defp get_func_docs_from_module_info(mod, fun, call_arity) do + # it is not worth doing fallback to behaviours here + # we'll not get much more useful info + + # provide dummy docs basing on module_info(:exports) + for {f, {arity, _kind}} <- Introspection.get_exports(mod), + f == fun, + Introspection.matches_arity?(arity, call_arity) do + fun_args_text = + if arity == 0, do: [], else: Enum.map(1..arity, fn _ -> "term" end) + + metadata = + if {f, arity} in BuiltinFunctions.erlang_builtin_functions(mod) do + %{builtin: true, app: :erts} + else + # TODO remove this fallback? + app = ElixirSense.Core.Applications.get_application(mod) + %{app: app} + end + + %{ + kind: :function, + module: mod, + function: fun, + arity: arity, + args: fun_args_text, + metadata: metadata, + specs: Introspection.get_specs_text(mod, fun, arity, :function, metadata), + docs: "" + } + end + end + + @spec get_type_docs(nil | module, atom, non_neg_integer | :any) :: list(type_doc()) + defp get_type_docs(nil, fun, arity) do + for info <- BuiltinTypes.get_builtin_type_info(fun), + Introspection.matches_arity?(length(info.params), arity) do + {spec, args} = + case info do + %{signature: signature, params: params} -> + {"@type #{signature}", Enum.map(params, &(&1 |> Atom.to_string()))} + + %{spec: spec_ast, params: params} -> + {TypeInfo.format_type_spec_ast(spec_ast, :type), + Enum.map(params, &(&1 |> Atom.to_string()))} + + _ -> + {"@type #{fun}()", []} + end + + %{ + kind: :type, + module: nil, + type: fun, + arity: length(info.params), + args: args, + metadata: %{builtin: true}, + spec: spec, + docs: info.doc + } + end + end + + defp get_type_docs(mod, fun, arity) do + docs = + (NormalizedCode.get_docs(mod, :type_docs) || []) + |> Enum.filter(fn {{name, n_args}, _, _, _, _} -> + name == fun and Introspection.matches_arity?(n_args, arity) + end) + |> Enum.sort_by(fn {{_, n_args}, _, _, _, _} -> n_args end) + + case docs do + [] -> + # TODO remove this fallback? + app = ElixirSense.Core.Applications.get_application(mod) + + for {kind, {name, _type, args}} = typedef <- Typespec.get_types(mod), + name == fun, + Introspection.matches_arity?(length(args), arity), + kind in [:type, :opaque] do + spec = TypeInfo.format_type_spec(typedef) + + type_args = Enum.map(args, &(&1 |> elem(2) |> Atom.to_string())) + + %{ + kind: :type, + module: mod, + type: fun, + arity: length(args), + args: type_args, + metadata: %{app: app}, + spec: spec, + docs: "" + } + end + + docs -> + for {{f, arity}, _, _, text, metadata} <- docs, f == fun do + spec = + mod + |> TypeInfo.get_type_spec(f, arity) + + {_kind, {_name, _def, args}} = spec + type_args = Enum.map(args, &(&1 |> elem(2) |> Atom.to_string())) + + %{ + kind: :type, + module: mod, + type: fun, + arity: arity, + args: type_args, + metadata: metadata, + spec: TypeInfo.format_type_spec(spec), + docs: text || "" + } + end + end + end + + def expand({{:atom, module}, func}, aliases) do + {Introspection.expand_alias(module, aliases), func} + end + + def expand({nil, func}, _aliases) do + {nil, func} + end + + def expand({:none, func}, _aliases) do + {nil, func} + end + + def expand({_, _func}, _aliases) do + {nil, nil} + end +end diff --git a/apps/language_server/lib/language_server/providers/implementation.ex b/apps/language_server/lib/language_server/providers/implementation.ex index b737d0b31..f570a68a5 100644 --- a/apps/language_server/lib/language_server/providers/implementation.ex +++ b/apps/language_server/lib/language_server/providers/implementation.ex @@ -4,6 +4,7 @@ defmodule ElixirLS.LanguageServer.Providers.Implementation do """ alias ElixirLS.LanguageServer.{Protocol, Parser} + alias ElixirLS.LanguageServer.Providers.Implementation.Locator def implementation( uri, @@ -12,7 +13,7 @@ defmodule ElixirLS.LanguageServer.Providers.Implementation do character, project_dir ) do - locations = ElixirSense.implementations(source_file.text, line, character, metadata: metadata) + locations = Locator.implementations(source_file.text, line, character, metadata: metadata) results = for location <- locations, diff --git a/apps/language_server/lib/language_server/providers/implementation/locator.ex b/apps/language_server/lib/language_server/providers/implementation/locator.ex new file mode 100644 index 000000000..f5dc539fe --- /dev/null +++ b/apps/language_server/lib/language_server/providers/implementation/locator.ex @@ -0,0 +1,345 @@ +defmodule ElixirLS.LanguageServer.Providers.Implementation.Locator do + @moduledoc """ + Provides a function to find out where symbols are implemented. + """ + + alias ElixirSense.Core.Behaviours + alias ElixirSense.Core.Binding + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Normalized + alias ElixirSense.Core.State + alias ElixirSense.Core.State.ModFunInfo + alias ElixirSense.Core.SurroundContext + alias ElixirLS.LanguageServer.Location + alias ElixirSense.Core.Parser + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode + + require ElixirSense.Core.Introspection, as: Introspection + + def implementations(code, line, column, options \\ []) do + case NormalizedCode.Fragment.surround_context(code, {line, column}) do + :none -> + [] + + context -> + metadata = + Keyword.get_lazy(options, :metadata, fn -> + Parser.parse_string(code, true, true, {line, column}) + end) + + env = Metadata.get_env(metadata, {line, column}) + + find( + context, + env, + metadata + ) + end + end + + @doc """ + Finds out where a callback, protocol or delegate was implemented. + """ + @spec find( + any(), + State.Env.t(), + Metadata.t() + ) :: [%Location{}] + def find( + context, + %State.Env{ + module: module + } = env, + metadata + ) do + binding_env = Binding.from_env(env, metadata) + + type = SurroundContext.to_binding(context.context, module) + + case type do + nil -> + [] + + {kind, _} when kind in [:attribute, :keyword] -> + [] + + {module_type, function} -> + module = + case Binding.expand(binding_env, module_type) do + {:atom, module} -> + Introspection.expand_alias(module, env.aliases) + + _ -> + env.module + end + + {line, column} = context.end + call_arity = Metadata.get_call_arity(metadata, module, function, line, column) || :any + + behaviour_implementations = + find_behaviour_implementations( + module, + function, + call_arity, + module, + env, + metadata, + binding_env + ) + + if behaviour_implementations == [] do + find_delegatee( + {module, function}, + call_arity, + env, + metadata, + binding_env + ) + |> List.wrap() + else + behaviour_implementations + end + end + end + + def find_behaviour_implementations( + maybe_found_module, + maybe_fun, + arity, + module, + env, + metadata, + binding_env + ) do + case maybe_found_module || module do + nil -> + [] + + found_module -> + found_module = expand(found_module, binding_env) + + cond do + maybe_fun == nil or Introspection.is_callback(found_module, maybe_fun, arity, metadata) -> + # protocol function call + get_locations(found_module, maybe_fun, arity, metadata) + + maybe_fun != nil -> + behaviours = Metadata.get_module_behaviours(metadata, env, module) + + # callback/protocol implementation def + for behaviour <- behaviours, + Introspection.is_callback(behaviour, maybe_fun, arity, metadata) do + get_locations(behaviour, maybe_fun, arity, metadata) + end + |> List.flatten() + + true -> + [] + end + end + |> Enum.reject(&is_nil/1) + end + + defp expand({kind, _attr} = type, binding_env) when kind in [:attribute, :variable] do + case Binding.expand(binding_env, type) do + {:atom, atom} -> atom + _ -> nil + end + end + + defp expand(other, _binding_env), do: other + + defp get_locations(behaviour, maybe_callback, arity, metadata) do + metadata_implementations = + for {_, env} <- metadata.lines_to_env, + behaviour in env.behaviours, + module <- env.module_variants, + uniq: true, + do: module + + metadata_implementations_locations = + metadata_implementations + |> Enum.map(fn module -> + {{line, column}, info} = + metadata.mods_funs_to_positions + |> Enum.find_value(fn + {{^module, ^maybe_callback, _}, info} when is_nil(maybe_callback) -> + {List.last(info.positions), info} + + {{^module, ^maybe_callback, a}, info} when not is_nil(a) -> + defaults = info.params |> List.last() |> Introspection.count_defaults() + + if Introspection.matches_arity_with_defaults?(a, defaults, arity) do + {List.last(info.positions), info} + end + + _ -> + nil + end) + + kind = ModFunInfo.get_category(info) + + {module, %Location{type: kind, file: nil, line: line, column: column}} + end) + + introspection_implementations_locations = + Behaviours.get_all_behaviour_implementations(behaviour) + |> Enum.map(fn implementation -> + {implementation, Location.find_mod_fun_source(implementation, maybe_callback, arity)} + end) + + Keyword.merge(introspection_implementations_locations, metadata_implementations_locations) + |> Keyword.values() + end + + defp find_delegatee( + mf, + arity, + env, + metadata, + binding_env, + visited \\ [] + ) do + unless mf in visited do + do_find_delegatee( + mf, + arity, + env, + metadata, + binding_env, + [mf | visited] + ) + end + end + + defp do_find_delegatee( + {{kind, _} = type, function}, + arity, + env, + metadata, + binding_env, + visited + ) + when kind in [:attribute, :variable] do + case Binding.expand(binding_env, type) do + {:atom, module} -> + do_find_delegatee( + {Introspection.expand_alias(module, env.aliases), function}, + arity, + env, + metadata, + binding_env, + visited + ) + + _ -> + nil + end + end + + defp do_find_delegatee( + {module, function}, + arity, + env, + metadata, + binding_env, + visited + ) do + %State.Env{ + module: current_module, + imports: imports, + requires: requires, + aliases: aliases, + scope: scope + } = env + + case {module, function} + |> Introspection.actual_mod_fun( + imports, + requires, + aliases, + current_module, + scope, + metadata.mods_funs_to_positions, + metadata.types, + # we don't expect macros here so no need to check position + {1, 1} + ) do + {mod, fun, true, :mod_fun} when not is_nil(fun) -> + # on defdelegate - no need for arity fallback here + info = + Location.get_function_position_using_metadata( + mod, + fun, + arity, + metadata.mods_funs_to_positions + ) + + case info do + nil -> + find_delegatee_location(mod, fun, arity, visited) + + %ModFunInfo{type: :defdelegate, target: target} when not is_nil(target) -> + find_delegatee( + target, + arity, + env, + metadata, + binding_env, + visited + ) + + %ModFunInfo{positions: positions, type: :def} -> + # find_delegatee_location(mod, fun, arity, visited) + if length(visited) > 1 do + {line, column} = List.last(positions) + %Location{type: :function, file: nil, line: line, column: column} + end + + _ -> + nil + end + + _ -> + nil + end + end + + defp find_delegatee_location(mod, fun, arity, visited) do + defdelegate_from_docs = get_defdelegate_by_docs(mod, fun, arity) + + case defdelegate_from_docs do + nil -> + # ensure we are expanding a delegate + if length(visited) > 1 do + # on defdelegate - no need for arity fallback + Location.find_mod_fun_source(mod, fun, arity) + end + + {_, _, _, _, _, + %{ + delegate_to: {delegate_mod, delegate_fun, delegate_arity} + }} -> + # on call of delegated function - arity fallback already done + Location.find_mod_fun_source(delegate_mod, delegate_fun, delegate_arity) + end + end + + defp get_defdelegate_by_docs(mod, fun, arity) do + Normalized.Code.get_docs(mod, :docs) + |> List.wrap() + |> Enum.filter(fn + {{^fun, a}, _, :function, _, _, %{delegate_to: _} = meta} -> + default_args = Map.get(meta, :defaults, 0) + Introspection.matches_arity_with_defaults?(a, default_args, arity) + + _ -> + false + end) + |> Enum.min_by( + fn {{_, a}, _, _, _, _, _} -> a end, + &<=/2, + fn -> nil end + ) + end +end diff --git a/apps/language_server/lib/language_server/providers/references.ex b/apps/language_server/lib/language_server/providers/references.ex index 00f70899a..e03302128 100644 --- a/apps/language_server/lib/language_server/providers/references.ex +++ b/apps/language_server/lib/language_server/providers/references.ex @@ -1,8 +1,7 @@ defmodule ElixirLS.LanguageServer.Providers.References do @moduledoc """ - This module provides textDocument/references support by using `ElixirSense.references/3` to - find all references to any function or module identified at the provided - location. + This module provides textDocument/references support. Currently its able to find references to + functions, macros, variables and module attributes Does not support configuring "includeDeclaration" and assumes it is always `true` @@ -12,6 +11,7 @@ defmodule ElixirLS.LanguageServer.Providers.References do alias ElixirLS.LanguageServer.{SourceFile, Build, Parser} import ElixirLS.LanguageServer.Protocol + alias ElixirLS.LanguageServer.Providers.References.Locator require Logger def references( @@ -25,13 +25,13 @@ defmodule ElixirLS.LanguageServer.Providers.References do Build.with_build_lock(fn -> trace = ElixirLS.LanguageServer.Tracer.get_trace() - ElixirSense.references(source_file.text, line, character, trace, metadata: metadata) + Locator.references(source_file.text, line, character, trace, metadata: metadata) |> Enum.map(fn elixir_sense_reference -> elixir_sense_reference |> build_reference(uri, source_file.text, project_dir) end) |> Enum.filter(&(not is_nil(&1))) - # ElixirSense returns references from both compile tracer and current buffer + # Returned references come from both compile tracer and current buffer # There may be duplicates |> Enum.uniq() end) diff --git a/apps/language_server/lib/language_server/providers/references/locator.ex b/apps/language_server/lib/language_server/providers/references/locator.ex new file mode 100644 index 000000000..a48ea0c4a --- /dev/null +++ b/apps/language_server/lib/language_server/providers/references/locator.ex @@ -0,0 +1,444 @@ +defmodule ElixirLS.LanguageServer.Providers.References.Locator do + @moduledoc """ + This module provides References to function or module identified at the provided location. + """ + + alias ElixirSense.Core.Binding + require ElixirSense.Core.Introspection, as: Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode + alias ElixirSense.Core.State + alias ElixirSense.Core.State.AttributeInfo + alias ElixirSense.Core.State.VarInfo + alias ElixirSense.Core.SurroundContext + alias ElixirSense.Core.Parser + + def references(code, line, column, trace, options \\ []) do + case NormalizedCode.Fragment.surround_context(code, {line, column}) do + :none -> + [] + + %{ + begin: {begin_line, begin_col} + } = context -> + metadata = + Keyword.get_lazy(options, :metadata, fn -> + Parser.parse_string(code, true, true, {line, column}) + end) + + env = + %State.Env{ + module_variants: module_variants + } = + Metadata.get_env(metadata, {line, column}) + |> Metadata.add_scope_vars(metadata, {line, column}) + + # find last env of current module + attributes = get_attributes(metadata, module_variants) + + # one line can contain variables from many scopes + # if the cursor is over variable take variables from the scope as it will + # be more correct than the env scope + vars = + case Enum.find(env.vars, fn %VarInfo{positions: positions} -> + {begin_line, begin_col} in positions + end) do + %VarInfo{scope_id: scope_id} -> + # in (h|l)?eex templates vars_info_per_scope_id[scope_id] is nil + if metadata.vars_info_per_scope_id[scope_id] do + metadata.vars_info_per_scope_id[scope_id] + else + [] + end + + nil -> + [] + end + + find( + context, + env, + vars, + attributes, + metadata, + trace + ) + end + end + + defp get_attributes(_metadata, []), do: [] + + defp get_attributes(metadata, [module | _]) do + %State.Env{attributes: attributes} = Metadata.get_last_module_env(metadata, module) + + attributes + end + + @type position :: %{line: pos_integer, column: pos_integer} + + @type range :: %{ + start: position, + end: position + } + + @type reference_info :: %{ + uri: String.t() | nil, + range: range + } + + def find( + context, + %State.Env{ + imports: imports, + requires: requires, + aliases: aliases, + module: module, + scope: scope + } = env, + vars, + attributes, + %Metadata{ + mods_funs_to_positions: mods_funs, + calls: calls, + types: metadata_types + } = metadata, + trace + ) do + binding_env = Binding.from_env(env, metadata) + + type = SurroundContext.to_binding(context.context, module) + + refs_for_mod_fun = fn {mod, function} -> + actual = + {mod, function} + |> expand(binding_env, module, aliases) + |> Introspection.actual_mod_fun( + imports, + requires, + aliases, + module, + scope, + mods_funs, + metadata_types, + context.begin + ) + + case actual do + {mod, fun, true, :mod_fun} -> + {line, column} = context.end + call_arity = Metadata.get_call_arity(metadata, module, function, line, column) || :any + + metadata_call_references = + calls + |> Map.values() + |> List.flatten() + |> Enum.filter(fn call -> function == nil or call.func == function end) + |> Enum.map(fn call -> + env = Metadata.get_env(metadata, call.position) + + binding_env = Binding.from_env(env, metadata) + + found = + {call.mod, function} + |> wrap_atom + |> expand(binding_env, module, aliases) + |> Introspection.actual_mod_fun( + env.imports, + env.requires, + env.aliases, + env.module, + env.scope, + mods_funs, + metadata_types, + call.position + ) + + case found do + {^mod, ^function, true, :mod_fun} when is_nil(function) -> + build_var_location(to_string(call.func), call.position) + + {^mod, ^function, true, :mod_fun} -> + [_, _, arities] = get_matching_arities([mod, function, call_arity], mods_funs) + corrected_arity = get_corrected_arity([mod, function, call.arity], mods_funs) + + if Enum.any?( + arities, + &Introspection.matches_arity?(corrected_arity, &1) + ) do + build_var_location(to_string(function), call.position) + end + + _ -> + nil + end + end) + |> Enum.filter(&(not is_nil(&1))) + + tracer_call_reverences = + {mod, fun} + |> xref_at_cursor(call_arity, module, scope, mods_funs, trace) + |> Enum.map(&build_location/1) + + (metadata_call_references ++ tracer_call_reverences) + |> Enum.sort_by(fn %{uri: a, range: %{start: %{line: b, column: c}}} -> {a, b, c} end) + + _ -> + # no results for types or not found + [] + end + end + + case type do + nil -> + [] + + {:keyword, _} -> + [] + + {:variable, variable} -> + {line, column} = context.begin + + var_info = + Enum.find(vars, fn %VarInfo{name: name, positions: positions} -> + name == variable and {line, column} in positions + end) + + if var_info != nil do + %VarInfo{positions: positions} = var_info + + positions + |> Enum.map(fn pos -> build_var_location(to_string(variable), pos) end) + else + refs_for_mod_fun.({nil, variable}) + end + + {:attribute, attribute} -> + attribute_info = + attributes + |> Enum.find(fn %AttributeInfo{name: name} -> name == attribute end) + + if attribute_info != nil do + %AttributeInfo{positions: positions} = attribute_info + + positions + |> Enum.map(fn pos -> build_var_location("@#{attribute}", pos) end) + else + [] + end + + {mod, function} -> + refs_for_mod_fun.({mod, function}) + end + end + + defp xref_at_cursor(actual_mod_fun, arity, module, scope, mods_funs, trace) do + mfa = callee_at_cursor(actual_mod_fun, module, scope, arity, mods_funs) + + filtered_calls(mfa, mods_funs, trace) + end + + # Cursor over a module + defp callee_at_cursor({module, nil}, _module, _scope, _arity, _mods_funs) do + [module] + end + + # Cursor over a function call + defp callee_at_cursor({module, func}, _module, _scope, arity, _mods_funs) do + [module, func, arity] + end + + defp filtered_calls(mfa, mods_funs, trace) do + mfa = get_matching_arities(mfa, mods_funs) + + trace + |> Map.values() + |> List.flatten() + |> Enum.filter(caller_filter(mfa, mods_funs)) + |> Enum.uniq() + end + + defp caller_filter([module, func, filter_arities], mods_funs) do + fn + %{callee: {^module, ^func, callee_arity}} -> + corrected_arity = get_corrected_arity([module, func, callee_arity], mods_funs) + Enum.any?(filter_arities, &Introspection.matches_arity?(corrected_arity, &1)) + + _ -> + false + end + end + + defp caller_filter([module, func], _mods_funs), do: &match?(%{callee: {^module, ^func, _}}, &1) + defp caller_filter([module], _mods_funs), do: &match?(%{callee: {^module, _, _}}, &1) + + defp build_location(call) do + %{callee: {_, func, _}} = call + + line = call.line || 1 + + {start_column, end_column} = + if call.line != nil and call.column != nil do + func_length = func |> to_string() |> String.length() + {call.column, call.column + func_length} + else + {1, 1} + end + + %{ + uri: call.file, + range: %{ + start: %{line: line, column: start_column}, + end: %{line: line, column: end_column} + } + } + end + + defp build_var_location(subject, {line, column}) do + %{ + uri: nil, + range: %{ + start: %{line: line, column: column}, + end: %{line: line, column: column + String.length(subject)} + } + } + end + + defp expand({nil, func}, _env, module, _aliases) when module not in [nil, Elixir], + do: {nil, func} + + defp expand({type, func}, env, _module, aliases) do + case Binding.expand(env, type) do + {:atom, module} -> {Introspection.expand_alias(module, aliases), func} + _ -> {nil, nil} + end + end + + defp get_corrected_arity([m], _mods_funs) do + [m] + end + + defp get_corrected_arity([m, f, a], mods_funs) do + arity = + mods_funs + |> Enum.find_value(fn + {{^m, ^f, arity}, info} when not is_nil(arity) -> + # no need to filter public only here + defaults = info.params |> List.last() |> Introspection.count_defaults() + + if Introspection.matches_arity_with_defaults?(arity, defaults, a) do + arity + end + + _ -> + false + end) + + arity = + if arity != nil do + arity + else + case NormalizedCode.get_docs(m, :docs) do + nil -> + nil + + docs -> + docs + |> Enum.find_value(fn + {{^f, arity}, _, _, _, _, meta} -> + defaults = Map.get(meta, :defaults, 0) + + if Introspection.matches_arity_with_defaults?(arity, defaults, a) do + arity + end + + _ -> + false + end) + end + end + + if arity != nil do + arity + else + # no need to drop macro prefix and correct arity - macros handled by docs + if Code.ensure_loaded?(m) and {f, a} in m.module_info(:exports) do + a + end + end + end + + defp get_matching_arities([m, f, a], mods_funs) do + arities = + mods_funs + |> Enum.filter(fn + {{^m, ^f, arity}, info} when not is_nil(arity) -> + # no need to filter public only here + defaults = info.params |> List.last() |> Introspection.count_defaults() + + if Introspection.matches_arity_with_defaults?(arity, defaults, a) do + arity + end + + _ -> + false + end) + |> Enum.map(fn + {{_, _, arity}, _} -> arity + end) + + arities = + if arities == [] do + case NormalizedCode.get_docs(m, :docs) do + nil -> + [] + + docs -> + docs + |> Enum.filter(fn + {{^f, arity}, _, _, _, _, meta} -> + defaults = Map.get(meta, :defaults, 0) + + if Introspection.matches_arity_with_defaults?(arity, defaults, a) do + arity + end + + _ -> + false + end) + |> Enum.map(fn + {{^f, arity}, _, _, _, _, _meta} -> arity + end) + end + else + arities + end + + arities = + if arities == [] do + if Code.ensure_loaded?(m) do + m.module_info(:exports) + |> Enum.filter(fn {fun, arity} -> + # no need to drop macro prefix and correct arity - macros handled by docs + fun == f and Introspection.matches_arity?(arity, a) + end) + |> Enum.map(fn {_fun, arity} -> + arity + end) + else + [] + end + else + arities + end + + [m, f, arities] + end + + defp get_matching_arities(other, _mods_funs) do + other + end + + defp wrap_atom({nil, other}), do: {nil, other} + defp wrap_atom({atom, other}) when is_atom(atom), do: {{:atom, atom}, other} + defp wrap_atom(other), do: other +end diff --git a/apps/language_server/lib/language_server/providers/signature_help.ex b/apps/language_server/lib/language_server/providers/signature_help.ex index d422d346d..a41323dd9 100644 --- a/apps/language_server/lib/language_server/providers/signature_help.ex +++ b/apps/language_server/lib/language_server/providers/signature_help.ex @@ -3,6 +3,7 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp do Provider handling textDocument/signatureHelp """ alias ElixirLS.LanguageServer.{SourceFile, Parser} + alias ElixirLS.LanguageServer.Providers.SignatureHelp.Signature def trigger_characters(), do: ["(", ","] @@ -12,7 +13,7 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp do character ) do response = - case ElixirSense.signature(source_file.text, line, character, metadata: metadata) do + case Signature.signature(source_file.text, line, character, metadata: metadata) do %{active_param: active_param, signatures: signatures} -> %{ "activeSignature" => 0, diff --git a/apps/language_server/lib/language_server/providers/signature_help/signature.ex b/apps/language_server/lib/language_server/providers/signature_help/signature.ex new file mode 100644 index 000000000..b6c80a367 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/signature_help/signature.ex @@ -0,0 +1,185 @@ +defmodule ElixirLS.LanguageServer.Providers.SignatureHelp.Signature do + @moduledoc """ + Provider responsible for introspection information about function signatures. + """ + + alias ElixirSense.Core.Binding + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata + alias ElixirSense.Core.Source + alias ElixirSense.Core.State + alias ElixirSense.Core.TypeInfo + alias ElixirSense.Core.Parser + + @type signature_info :: %{ + active_param: non_neg_integer, + signatures: [Metadata.signature_t()] + } + + def signature(code, line, column, options \\ []) do + prefix = Source.text_before(code, line, column) + + metadata = + Keyword.get_lazy(options, :metadata, fn -> + Parser.parse_string(code, true, true, {line, column}) + end) + + env = Metadata.get_env(metadata, {line, column}) + + find(prefix, {line, column}, env, metadata) + end + + @doc """ + Returns the signature info from the function or type defined in the prefix, if any. + """ + @spec find(String.t(), {pos_integer, pos_integer}, State.Env.t(), Metadata.t()) :: + signature_info | :none + def find(prefix, cursor_position, env, metadata) do + %State.Env{ + imports: imports, + requires: requires, + aliases: aliases, + module: module, + scope: scope + } = env + + binding_env = Binding.from_env(env, metadata) + + with %{candidate: {m, f}, npar: npar, elixir_prefix: elixir_prefix} <- + Source.which_func(prefix, binding_env), + {mod, fun, true, kind} <- + Introspection.actual_mod_fun( + {m, f}, + imports, + requires, + if(elixir_prefix, do: [], else: aliases), + module, + scope, + metadata.mods_funs_to_positions, + metadata.types, + cursor_position + ) do + signatures = find_signatures({mod, fun}, npar, kind, env, metadata) + + %{active_param: npar, signatures: signatures} + else + _ -> + :none + end + end + + defp find_signatures({mod, fun}, npar, kind, env, metadata) do + signatures = + case kind do + :mod_fun -> find_function_signatures({mod, fun}, env, metadata) + :type -> find_type_signatures({mod, fun}, metadata) + end + + signatures + |> Enum.map(fn + %{params: []} = signature -> + if npar == 0 do + signature + end + + %{params: params} = signature -> + defaults = + params + |> Enum.with_index() + |> Enum.map(fn {param, index} -> {Regex.match?(~r/\\\\/u, param), index} end) + |> Enum.sort() + |> Enum.at(npar) + + case defaults do + nil -> nil + {_, index} when index != npar -> Map.put(signature, :active_param, index) + _ -> signature + end + end) + |> Enum.reject(&is_nil/1) + |> Enum.sort_by(&{length(&1.params), -Map.get(&1, :active_param, npar)}) + end + + defp find_function_signatures({nil, _fun}, _env, _metadata), do: [] + + defp find_function_signatures({mod, fun}, env, metadata) do + signatures = + case Metadata.get_function_signatures(metadata, mod, fun) do + [] -> + Introspection.get_signatures(mod, fun) + + signatures -> + for signature <- signatures do + arity = length(signature.params) + + behaviour_implementation = + Metadata.get_module_behaviours(metadata, env, mod) + |> Enum.find_value(fn behaviour -> + if Introspection.is_callback(behaviour, fun, arity, metadata) do + behaviour + end + end) + + case behaviour_implementation do + nil -> + signature + + behaviour -> + case metadata.specs[{behaviour, fun, arity}] do + %State.SpecInfo{} = spec_info -> + specs = + spec_info.specs + |> Enum.reject(&String.starts_with?(&1, "@spec")) + |> Enum.reverse() + + callback_doc = + case metadata.mods_funs_to_positions[{behaviour, fun, arity}] do + nil -> + spec_info.doc + + def_info -> + # in case of protocol implementation get doc and meta from def + def_info.doc + end + + %{ + signature + | spec: specs |> Enum.join("\n"), + documentation: Introspection.extract_summary_from_docs(callback_doc) + } + + nil -> + fun_info = Map.fetch!(metadata.mods_funs_to_positions, {mod, fun, arity}) + + {spec, doc, _} = + Metadata.get_doc_spec_from_behaviour( + behaviour, + fun, + arity, + State.ModFunInfo.get_category(fun_info) + ) + + %{ + signature + | documentation: Introspection.extract_summary_from_docs(doc), + spec: spec + } + end + end + end + end + + signatures |> Enum.uniq_by(fn sig -> sig.params end) + end + + defp find_type_signatures({nil, fun}, _metadata) do + TypeInfo.get_signatures(nil, fun) + end + + defp find_type_signatures({mod, fun}, metadata) do + case Metadata.get_type_signatures(metadata, mod, fun) do + [] -> TypeInfo.get_signatures(mod, fun) + signature -> signature + end + end +end diff --git a/apps/language_server/lib/language_server/providers/workspace_symbols.ex b/apps/language_server/lib/language_server/providers/workspace_symbols.ex index 9f7c49be8..78a0761e2 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -10,7 +10,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do alias ElixirLS.LanguageServer.SourceFile alias ElixirLS.LanguageServer.Providers.SymbolUtils alias ElixirLS.LanguageServer.JsonRpc - alias ElixirSense.Providers.Suggestion.Matcher + alias ElixirLS.Utils.Matcher require ElixirSense.Core.Introspection, as: Introspection require Logger @@ -395,9 +395,9 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do # docs = ElixirSense.Core.Normalized.Code.get_docs(module, :moduledoc) # # fetching docs is quite costly, since we already do it here we can use it to fill up caches # if ElixirSense.Core.Introspection.elixir_module?(module) do - # ElixirSense.Providers.Suggestion.Complete.fill_elixir_module_cache(module, docs) + # ElixirLS.LanguageServer.Providers.Completion.Complete.fill_elixir_module_cache(module, docs) # else - # ElixirSense.Providers.Suggestion.Complete.fill_erlang_module_cache(module, docs) + # ElixirLS.LanguageServer.Providers.Completion.Complete.fill_erlang_module_cache(module, docs) # end # TODO @moduledoc location diff --git a/apps/language_server/test/location_test.exs b/apps/language_server/test/location_test.exs new file mode 100644 index 000000000..46961609e --- /dev/null +++ b/apps/language_server/test/location_test.exs @@ -0,0 +1,47 @@ +defmodule ElixirLS.LanguageServer.LocationTest do + use ExUnit.Case, async: false + + alias ElixirLS.LanguageServer.Location + + setup do + elixir_src = Path.join(__DIR__, "/misc/mock_elixir_src") + # TODO make this work and expose via config + Application.put_env(:language_server, :elixir_src, elixir_src) + + on_exit(fn -> + Application.delete_env(:language_server, :elixir_src) + end) + end + + describe "find_mod_fun_source/3" do + test "returns location of a core Elixir function" do + if not File.exists?(String.module_info(:compile)[:source]) do + assert %Location{type: :function, line: 26, column: 3, file: file} = + Location.find_mod_fun_source(String, :length, 1) + + assert String.ends_with?(file, "/mock_elixir_src/lib/elixir/lib/string.ex") + else + assert %Location{type: :function, file: file} = + Location.find_mod_fun_source(String, :length, 1) + + assert file == Path.expand(String.module_info(:compile)[:source]) + end + end + end + + describe "find_type_source/3" do + test "returns location of a core Elixir type" do + if not File.exists?(String.module_info(:compile)[:source]) do + assert %Location{type: :typespec, line: 11, column: 3, file: file} = + Location.find_type_source(String, :t, 0) + + assert String.ends_with?(file, "/mock_elixir_src/lib/elixir/lib/string.ex") + else + assert %Location{type: :typespec, file: file} = + Location.find_type_source(String, :t, 0) + + assert file == Path.expand(String.module_info(:compile)[:source]) + end + end + end +end diff --git a/apps/language_server/test/misc/mock_elixir_src/lib/elixir/lib/string.ex b/apps/language_server/test/misc/mock_elixir_src/lib/elixir/lib/string.ex new file mode 100644 index 000000000..221ff5b16 --- /dev/null +++ b/apps/language_server/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 diff --git a/apps/language_server/test/providers/completion/suggestions_test.exs b/apps/language_server/test/providers/completion/suggestions_test.exs new file mode 100644 index 000000000..bc3918d0e --- /dev/null +++ b/apps/language_server/test/providers/completion/suggestions_test.exs @@ -0,0 +1,4805 @@ +defmodule ElixirLS.LanguageServer.Providers.Completion.SuggestionTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.Source + alias ElixirLS.LanguageServer.Providers.Completion.Suggestion + + import ExUnit.CaptureIO + + test "empty hint" do + buffer = """ + defmodule MyModule do + + end + """ + + list = Suggestion.suggestions(buffer, 2, 7) + + assert %{ + args: "module, opts", + args_list: ["module", "opts"], + arity: 2, + def_arity: 2, + name: "import", + origin: "Kernel.SpecialForms", + spec: "", + summary: "Imports functions and macros from other modules.", + type: :macro, + metadata: %{}, + snippet: nil, + visibility: :public + } = Enum.find(list, fn s -> match?(%{name: "import", arity: 2}, s) end) + + assert %{ + arity: 2, + def_arity: 2, + origin: "Kernel.SpecialForms", + spec: "", + type: :macro, + args: "opts, block", + args_list: ["opts", "block"], + name: "quote", + summary: "Gets the representation of any expression.", + metadata: %{}, + snippet: nil, + visibility: :public + } = Enum.find(list, fn s -> match?(%{name: "quote", arity: 2}, s) end) + + assert %{ + arity: 2, + def_arity: 2, + origin: "Kernel.SpecialForms", + spec: "", + type: :macro, + args: "module, opts", + args_list: ["module", "opts"], + name: "require", + metadata: %{}, + snippet: nil, + visibility: :public + } = Enum.find(list, fn s -> match?(%{name: "require", arity: 2}, s) end) + end + + test "without empty hint" do + buffer = """ + defmodule MyModule do + is_b + end + """ + + list = Suggestion.suggestions(buffer, 2, 7) + + assert [ + %{ + name: "is_binary", + origin: "Kernel", + arity: 1 + }, + %{ + name: "is_bitstring", + origin: "Kernel", + arity: 1 + }, + %{ + name: "is_boolean", + origin: "Kernel", + arity: 1 + }, + %{ + name: "is_number", + origin: "Kernel", + arity: 1 + } + ] = list + end + + test "capture hint" do + buffer = """ + defmodule MyModule do + @attr "asd" + def a(arg) do + arg + |> Enum.filter(&) + end + end + """ + + list = Suggestion.suggestions(buffer, 5, 21) + + assert list |> Enum.any?(&(&1.type == :module)) + assert list |> Enum.any?(&(&1.type == :function)) + assert list |> Enum.any?(&(&1.type == :variable)) + assert list |> Enum.any?(&(&1.type == :attribute)) + end + + test "pin hint 1" do + buffer = """ + defmodule MyModule do + @attr "asd" + def a(arg) do + case x() do + {^} -> :ok + end + end + end + """ + + list = Suggestion.suggestions(buffer, 5, 9) + + refute list |> Enum.any?(&(&1.type == :module)) + refute list |> Enum.any?(&(&1.type == :function)) + assert list |> Enum.any?(&(&1.type == :variable)) + refute list |> Enum.any?(&(&1.type == :attribute)) + end + + test "pin hint 2" do + buffer = """ + defmodule MyModule do + @attr "asd" + def a(arg) do + with ^ <- abc(), + x <- cde(), + y <- efg() do + :ok + end + end + end + """ + + list = Suggestion.suggestions(buffer, 4, 11) + + refute list |> Enum.any?(&(&1.type == :module)) + refute list |> Enum.any?(&(&1.type == :function)) + assert list |> Enum.any?(&(&1.type == :variable)) + refute list |> Enum.any?(&(&1.type == :attribute)) + end + + test "pin hint 3" do + buffer = """ + defmodule MyModule do + @attr "asd" + def a(arg) do + with {^} <- abc(), + x <- cde(), + y <- efg() do + :ok + end + end + end + """ + + list = Suggestion.suggestions(buffer, 4, 12) + + refute list |> Enum.any?(&(&1.type == :module)) + refute list |> Enum.any?(&(&1.type == :function)) + assert list |> Enum.any?(&(&1.type == :variable)) + refute list |> Enum.any?(&(&1.type == :attribute)) + end + + test "pin hint 4" do + buffer = """ + defmodule MyModule do + @attr "asd" + def a(arg) do + with a <- abc(), + x <- cde(), + y <- efg() do + :ok + else + ^ -> :ok + :ok -> :ok + end + end + end + """ + + list = Suggestion.suggestions(buffer, 9, 8) + + refute list |> Enum.any?(&(&1.type == :module)) + refute list |> Enum.any?(&(&1.type == :function)) + assert list |> Enum.any?(&(&1.type == :variable)) + refute list |> Enum.any?(&(&1.type == :attribute)) + end + + test "no typespecs in function scope" do + buffer = """ + defmodule MyModule do + def go, do: + end + """ + + list = Suggestion.suggestions(buffer, 2, 15) + + refute list |> Enum.any?(&(&1.type == :type_spec)) + assert list |> Enum.any?(&(&1.type == :function)) + end + + test "functions from unicode module" do + buffer = """ + defmodule :你好 do + def 运行 do + IO.puts("你好") + end + end + + :你好. + """ + + list = Suggestion.suggestions(buffer, 7, 5) + + assert list |> Enum.any?(&(&1.type == :function && &1.name == "运行")) + end + + test "with an alias" do + buffer = """ + defmodule MyModule do + alias List, as: MyList + MyList.flat + end + """ + + list = Suggestion.suggestions(buffer, 3, 14) + + assert [ + %{ + args: "list", + args_list: ["list"], + arity: 1, + def_arity: 1, + name: "flatten", + origin: "List", + spec: "@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]", + summary: "Flattens the given `list` of nested lists.", + type: :function, + metadata: %{}, + visibility: :public, + snippet: nil + }, + %{ + args: "list, tail", + args_list: ["list", "tail"], + arity: 2, + def_arity: 2, + name: "flatten", + origin: "List", + spec: + "@spec flatten(deep_list, [elem]) :: [elem] when deep_list: [elem | deep_list], elem: var", + summary: + "Flattens the given `list` of nested lists.\nThe list `tail` will be added at the end of\nthe flattened list.", + type: :function, + metadata: %{}, + visibility: :public, + snippet: nil + } + ] = list + end + + test "with a require" do + buffer = """ + defmodule MyModule do + require ElixirSenseExample.BehaviourWithMacrocallback.Impl, as: Macros + Macros.so + end + """ + + list = Suggestion.suggestions(buffer, 3, 12) + + assert [ + %{ + args: "var", + args_list: ["var"], + arity: 1, + def_arity: 1, + name: "some", + origin: "ElixirSenseExample.BehaviourWithMacrocallback.Impl", + spec: + "@spec some(integer()) :: Macro.t()\n@spec some(b) :: Macro.t() when b: float()", + summary: "some macro\n", + type: :macro, + metadata: %{}, + snippet: nil, + visibility: :public + } + ] = list + end + + test "with a module hint" do + buffer = """ + defmodule MyModule do + ElixirSenseExample.ModuleWithDo + end + """ + + list = Suggestion.suggestions(buffer, 2, 34) + + assert [ + %{ + name: "ModuleWithDocFalse", + full_name: "ElixirSenseExample.ModuleWithDocFalse", + subtype: nil, + summary: "", + type: :module, + metadata: %{} + }, + %{ + name: "ModuleWithDocs", + full_name: "ElixirSenseExample.ModuleWithDocs", + subtype: :behaviour, + summary: "An example module\n", + type: :module, + metadata: %{since: "1.2.3"} + }, + %{ + metadata: %{}, + name: "ModuleWithNoDocs", + full_name: "ElixirSenseExample.ModuleWithNoDocs", + subtype: nil, + summary: "", + type: :module + } + ] = list + end + + test "lists metadata modules" do + buffer = """ + defmodule MyServer do + @moduledoc "Some" + @moduledoc since: "1.2.3" + end + MySe + """ + + list = + Suggestion.suggestions(buffer, 5, 5) + |> Enum.filter(fn s -> s.type == :module end) + + assert [ + %{ + name: "MyServer", + summary: "Some", + type: :module, + full_name: "MyServer", + metadata: %{since: "1.2.3"}, + required_alias: nil, + subtype: nil + } + ] = list + end + + test "returns subtype on local modules" do + buffer = """ + defprotocol MyProto do + end + MyPr + """ + + list = + Suggestion.suggestions(buffer, 3, 5) + |> Enum.filter(fn s -> s.type == :module end) + + assert [ + %{ + name: "MyProto", + subtype: :protocol + } + ] = list + end + + test "lists callbacks" do + buffer = """ + defmodule MyServer do + use GenServer + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 7) + |> Enum.filter(fn s -> s.type == :callback && s.name == "code_change" end) + + assert [ + %{ + args: "old_vsn, state, extra", + arity: 3, + name: "code_change", + origin: "GenServer", + spec: "@callback code_change(old_vsn, state :: term(), extra :: term()) ::" <> _, + summary: + "Invoked to change the state of the `GenServer` when a different version of a\nmodule is loaded (hot code swapping) and the state's term structure should be\nchanged.", + type: :callback + } + ] = list + end + + test "lists metadata behaviour callbacks" do + buffer = """ + defmodule MyBehaviour do + @doc "Some callback" + @callback my_callback(integer()) :: any() + + @callback my_callback_optional(integer(), atom()) :: any() + + @deprecated "Replace me" + @macrocallback my_macrocallback(integer()) :: Macro.t() + + @optional_callbacks my_callback_optional: 2 + end + + defmodule MyServer do + @behaviour MyBehaviour + + end + """ + + list = + Suggestion.suggestions(buffer, 15, 3) + |> Enum.filter(fn s -> s.type == :callback end) + + assert [ + %{ + args: "integer()", + arity: 1, + name: "my_callback", + origin: "MyBehaviour", + spec: "@callback my_callback(integer()) :: any()", + summary: "Some callback", + type: :callback, + args_list: ["integer()"], + metadata: %{}, + subtype: :callback + }, + %{ + args: "integer()", + args_list: ["integer()"], + arity: 1, + metadata: %{deprecated: "Replace me"}, + name: "my_macrocallback", + origin: "MyBehaviour", + spec: "@macrocallback my_macrocallback(integer()) :: Macro.t()", + subtype: :macrocallback, + summary: "", + type: :callback + }, + %{ + args: "integer(), atom()", + args_list: ["integer()", "atom()"], + arity: 2, + metadata: %{optional: true}, + name: "my_callback_optional", + origin: "MyBehaviour", + spec: "@callback my_callback_optional(integer(), atom()) :: any()", + subtype: :callback, + summary: "", + type: :callback + } + ] = list + end + + test "lists metadata protocol functions" do + buffer = """ + defprotocol MyProto do + @doc "Some callback" + @doc since: "1.2.3" + def my_fun(t) + + @doc deprecated: "1.2.3" + @spec my_fun_other(t(), integer()) :: any() + def my_fun_other(t, a) + end + + defimpl MyProto, for: List do + + end + """ + + list = + Suggestion.suggestions(buffer, 11, 3) + |> Enum.filter(fn s -> s.type == :protocol_function end) + + assert [ + %{ + args: "t", + args_list: ["t"], + arity: 1, + metadata: %{since: "1.2.3"}, + name: "my_fun", + origin: "MyProto", + spec: "@callback my_fun(t) :: term", + summary: "Some callback", + type: :protocol_function + }, + %{ + args: "t(), integer()", + args_list: ["t()", "integer()"], + arity: 2, + metadata: %{deprecated: "1.2.3"}, + name: "my_fun_other", + origin: "MyProto", + spec: "@spec my_fun_other(t(), integer()) :: any()", + summary: "", + type: :protocol_function + } + ] = list + end + + test "lists callbacks + def macros after de" do + buffer = """ + defmodule MyServer do + use GenServer + + de + # ^ + end + """ + + list = Suggestion.suggestions(buffer, 4, 5) + assert Enum.any?(list, fn s -> s.type == :callback end) + assert Enum.any?(list, fn s -> s.type == :macro end) + assert Enum.all?(list, fn s -> s.type in [:callback, :macro] end) + end + + test "lists callbacks + def macros after def" do + buffer = """ + defmodule MyServer do + use GenServer + + def + # ^ + end + """ + + list = Suggestion.suggestions(buffer, 4, 6) + assert Enum.any?(list, fn s -> s.type == :callback end) + assert Enum.any?(list, fn s -> s.type == :macro end) + assert Enum.all?(list, fn s -> s.type in [:callback, :macro] end) + end + + test "lists only callbacks after def + space" do + buffer = """ + defmodule MyServer do + use GenServer + + def t + # ^ + end + """ + + assert Suggestion.suggestions(buffer, 4, 7) |> Enum.all?(fn s -> s.type == :callback end) + + buffer = """ + defmodule MyServer do + use GenServer + + def t + # ^ + end + """ + + assert [%{name: "terminate", type: :callback}] = Suggestion.suggestions(buffer, 4, 8) + end + + test "do not list callbacks inside functions" do + buffer = """ + defmodule MyServer do + use GenServer + + def init(_) do + t + # ^ + end + end + """ + + list = Suggestion.suggestions(buffer, 5, 6) + assert Enum.any?(list, fn s -> s.type == :function end) + refute Enum.any?(list, fn s -> s.type == :callback end) + end + + test "lists macrocallbacks" do + buffer = """ + defmodule MyServer do + @behaviour ElixirSenseExample.BehaviourWithMacrocallback + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 7) + |> Enum.filter(fn s -> s.type == :callback end) + + assert [ + %{ + args: "a", + args_list: ["a"], + arity: 1, + name: "optional", + subtype: :macrocallback, + origin: "ElixirSenseExample.BehaviourWithMacrocallback", + spec: "@macrocallback optional(a) :: Macro.t() when a: atom()", + summary: "An optional macrocallback\n", + type: :callback, + metadata: %{optional: true, app: :language_server} + }, + %{ + args: "atom", + args_list: ["atom"], + arity: 1, + name: "required", + subtype: :macrocallback, + origin: "ElixirSenseExample.BehaviourWithMacrocallback", + spec: "@macrocallback required(atom()) :: Macro.t()", + summary: "A required macrocallback\n", + type: :callback, + metadata: %{optional: false, app: :language_server} + } + ] == list + end + + test "lists macrocallbacks + def macros after defma" do + buffer = """ + defmodule MyServer do + @behaviour ElixirSenseExample.BehaviourWithMacrocallback + + defma + # ^ + end + """ + + list = Suggestion.suggestions(buffer, 4, 8) + assert Enum.any?(list, fn s -> s.type == :callback end) + assert Enum.any?(list, fn s -> s.type == :macro end) + assert Enum.all?(list, fn s -> s.type in [:callback, :macro] end) + end + + test "lists erlang callbacks" do + buffer = """ + defmodule MyServer do + @behaviour :gen_statem + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 7) + |> Enum.filter(fn s -> s.type == :callback && s.name == "code_change" end) + + assert [ + %{ + args: "oldVsn, oldState, oldData, extra", + arity: 4, + name: "code_change", + origin: ":gen_statem", + spec: "@callback code_change" <> _, + summary: summary, + type: :callback, + subtype: :callback + } + ] = list + + if System.otp_release() |> String.to_integer() >= 23 do + assert "- OldVsn = Vsn" <> _ = summary + end + end + + test "callback suggestions should not crash with unquote(__MODULE__)" do + buffer = """ + defmodule Dummy do + @doc false + defmacro __using__() do + quote location: :keep do + @behaviour unquote(__MODULE__) + end + end + end + """ + + assert [%{} | _] = Suggestion.suggestions(buffer, 8, 5) + end + + test "lists overridable callbacks" do + buffer = """ + defmodule MyServer do + use ElixirSenseExample.OverridableImplementation + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 7) + |> Enum.filter(fn s -> s.type == :callback end) + + assert [ + %{ + args: "", + arity: 0, + name: "foo", + origin: "ElixirSenseExample.OverridableBehaviour", + spec: "@callback foo() :: any()", + summary: "", + type: :callback, + subtype: :callback, + metadata: %{optional: false, overridable: true} + }, + %{ + args: "any", + arity: 1, + metadata: %{optional: false, overridable: true}, + name: "bar", + origin: "ElixirSenseExample.OverridableBehaviour", + spec: "@macrocallback bar(any()) :: Macro.t()", + subtype: :macrocallback, + summary: "", + type: :callback + } + ] = list + end + + test "lists overridable functions and macros" do + buffer = """ + defmodule MyServer do + use ElixirSenseExample.OverridableFunctions + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 7) + |> Enum.filter(fn s -> s.type == :callback end) + + assert [ + %{ + args: "var", + arity: 1, + metadata: %{overridable: true}, + name: "required", + origin: "ElixirSenseExample.OverridableFunctions", + spec: "", + summary: "", + type: :callback, + subtype: :macrocallback + }, + %{ + args: "x, y", + arity: 2, + metadata: %{since: "1.2.3", overridable: true}, + name: "test", + origin: "ElixirSenseExample.OverridableFunctions", + spec: "@spec test(number, number) :: number", + summary: "Some overridable", + type: :callback, + subtype: :callback + } + ] = list + end + + test "fuzzy match overridable functions" do + buffer = """ + defmodule MyServer do + use ElixirSenseExample.OverridableFunctions + + rqui + end + """ + + list = + Suggestion.suggestions(buffer, 4, 5) + |> Enum.filter(fn s -> s.type == :callback end) + + assert [ + %{ + args: "var", + arity: 1, + metadata: %{}, + name: "required", + origin: "ElixirSenseExample.OverridableFunctions", + spec: "", + summary: "", + type: :callback, + subtype: :macrocallback + } + ] = list + end + + test "lists protocol functions" do + buffer = """ + defimpl Enumerable, for: MyStruct do + + end + """ + + list = + Suggestion.suggestions(buffer, 2, 3) + |> Enum.filter(fn s -> s[:name] == "reduce" end) + + assert [ + %{ + args: "enumerable, acc, fun", + arity: 3, + name: "reduce", + origin: "Enumerable", + spec: "@callback reduce(t(), acc(), reducer()) :: result()", + summary: "Reduces the `enumerable` into an element.", + type: :protocol_function, + metadata: %{} + } + ] = list + end + + test "lists fuzzy protocol functions" do + buffer = """ + defimpl Enumerable, for: MyStruct do + reu + end + """ + + list = + Suggestion.suggestions(buffer, 2, 5) + |> Enum.filter(fn s -> s[:type] == :protocol_function end) + + assert [ + %{ + args: "enumerable, acc, fun", + arity: 3, + name: "reduce", + origin: "Enumerable", + spec: "@callback reduce(t(), acc(), reducer()) :: result()", + summary: "Reduces the `enumerable` into an element.", + type: :protocol_function, + metadata: %{} + } + ] = list + end + + test "lists callback return values" do + buffer = """ + defmodule MyServer do + use ElixirSenseExample.ExampleBehaviour + + def handle_call(request, from, state) do + + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 5) + |> Enum.filter(fn s -> s.type == :return end) + + assert [ + %{ + description: "{:reply, reply, new_state}", + snippet: "{:reply, \"${1:reply}$\", \"${2:new_state}$\"}", + spec: + "{:reply, reply, new_state} when reply: term(), new_state: term(), reason: term()", + type: :return + }, + %{ + description: + "{:reply, reply, new_state, timeout() | :hibernate | {:continue, term()}}", + snippet: + "{:reply, \"${1:reply}$\", \"${2:new_state}$\", \"${3:timeout() | :hibernate | {:continue, term()}}$\"}", + spec: + "{:reply, reply, new_state, timeout() | :hibernate | {:continue, term()}}" <> _, + type: :return + }, + %{ + description: "{:noreply, new_state}", + snippet: "{:noreply, \"${1:new_state}$\"}", + spec: + "{:noreply, new_state} when reply: term(), new_state: term(), reason: term()", + type: :return + }, + %{ + description: "{:noreply, new_state, timeout() | :hibernate | {:continue, term()}}", + snippet: + "{:noreply, \"${1:new_state}$\", \"${2:timeout() | :hibernate | {:continue, term()}}$\"}", + spec: "{:noreply, new_state, timeout() | :hibernate | {:continue, term()}}" <> _, + type: :return + }, + %{ + description: "{:stop, reason, reply, new_state}", + snippet: "{:stop, \"${1:reason}$\", \"${2:reply}$\", \"${3:new_state}$\"}", + spec: + "{:stop, reason, reply, new_state} when reply: term(), new_state: term(), reason: term()", + type: :return + }, + %{ + description: "{:stop, reason, new_state}", + snippet: "{:stop, \"${1:reason}$\", \"${2:new_state}$\"}", + spec: + "{:stop, reason, new_state} when reply: term(), new_state: term(), reason: term()", + type: :return + } + ] = list + end + + test "lists macrocallback return values" do + buffer = """ + defmodule MyServer do + @behaviour ElixirSenseExample.BehaviourWithMacrocallback + + defmacro required(arg) do + + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 5) + |> Enum.filter(fn s -> s.type == :return end) + + assert list == [ + %{ + description: "Macro.t()", + snippet: "\"${1:Macro.t()}$\"", + spec: "Macro.t()", + type: :return + } + ] + end + + test "lists metadata callback return values" do + buffer = """ + defmodule MyBehaviour do + @callback required(term()) :: {:ok, term()} | :error + end + + defmodule MyServer do + @behaviour MyBehaviour + + def required(arg) do + + end + end + """ + + list = + Suggestion.suggestions(buffer, 9, 5) + |> Enum.filter(fn s -> s.type == :return end) + + assert list == [ + %{ + description: "{:ok, term()}", + snippet: "{:ok, term()}", + spec: "{:ok, term()}", + type: :return + }, + %{description: ":error", snippet: ":error", spec: ":error", type: :return} + ] + end + + test "lists protocol implementation return values" do + buffer = """ + defimpl Enumerable, for: MyStruct do + def count(t) do + + end + end + """ + + list = + Suggestion.suggestions(buffer, 3, 6) + |> Enum.filter(fn s -> s.type == :return end) + + assert [ + %{ + description: "{:ok, non_neg_integer()}", + snippet: "{:ok, non_neg_integer()}", + spec: "{:ok, non_neg_integer()}", + type: :return + }, + %{ + description: "{:error, module()}", + snippet: "{:error, module()}", + spec: "{:error, module()}", + type: :return + } + ] == list + end + + test "lists metadata protocol implementation return values" do + buffer = """ + defprotocol MyProto do + @spec count(t()) :: {:ok, term()} | :error + def count(t) + end + + defimpl MyProto, for: MyStruct do + def count(t) do + + end + end + """ + + list = + Suggestion.suggestions(buffer, 8, 6) + |> Enum.filter(fn s -> s.type == :return end) + + assert [ + %{ + description: "{:ok, term()}", + snippet: "{:ok, term()}", + spec: "{:ok, term()}", + type: :return + }, + %{description: ":error", snippet: ":error", spec: ":error", type: :return} + ] == list + end + + test "lists function with spec return values" do + buffer = """ + defmodule SomeModule do + @spec count(atom) :: :ok | {:error, any} + def count(t) do + + end + end + """ + + list = + Suggestion.suggestions(buffer, 4, 6) + |> Enum.filter(fn s -> s.type == :return end) + + assert [ + %{description: ":ok", snippet: ":ok", spec: ":ok", type: :return}, + %{ + description: "{:error, any}", + snippet: "{:error, \"${1:any}$\"}", + spec: "{:error, any}", + type: :return + } + ] == list + end + + test "list metadata function - fallback to callback in metadata" do + buffer = """ + defmodule MyBehaviour do + @doc "Sample doc" + @doc since: "1.2.3" + @callback flatten(list()) :: list() + end + + defmodule MyLocalModule do + @behaviour MyBehaviour + + @impl true + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flat + end + end + """ + + list = + Suggestion.suggestions(buffer, 18, 23) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{ + args: "list", + arity: 1, + def_arity: 1, + metadata: %{implementing: MyBehaviour, hidden: true, since: "1.2.3"}, + name: "flatten", + origin: "MyLocalModule", + spec: "@callback flatten(list()) :: list()", + summary: "Sample doc", + type: :function, + visibility: :public + } + ] = list + end + + test "retrieve metadata function documentation - fallback to protocol function in metadata" do + buffer = """ + defprotocol BB do + @doc "asdf" + @spec go(t) :: integer() + def go(t) + end + + defimpl BB, for: String do + def go(t), do: "" + end + + defmodule MyModule do + def func(list) do + BB.String.go(list) + end + end + """ + + list = + Suggestion.suggestions(buffer, 13, 16) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{ + args: "t", + arity: 1, + def_arity: 1, + metadata: %{implementing: BB}, + name: "go", + origin: "BB.String", + spec: "@callback go(t) :: integer()", + summary: "asdf", + type: :function, + visibility: :public + } + ] = list + + # TODO docs and metadata + end + + test "list metadata macro - fallback to macrocallback in metadata" do + buffer = """ + defmodule MyBehaviour do + @doc "Sample doc" + @doc since: "1.2.3" + @macrocallback flatten(list()) :: list() + end + + defmodule MyLocalModule do + @behaviour MyBehaviour + + @impl true + defmacro flatten(list) do + [] + end + end + + defmodule MyModule do + require MyLocalModule + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + list = + Suggestion.suggestions(buffer, 19, 23) + |> Enum.filter(fn s -> s.type == :macro end) + + assert [ + %{ + args: "list", + arity: 1, + def_arity: 1, + metadata: %{implementing: MyBehaviour, hidden: true, since: "1.2.3"}, + name: "flatten", + origin: "MyLocalModule", + spec: "@macrocallback flatten(list()) :: list()", + summary: "Sample doc", + type: :macro, + visibility: :public + } + ] = list + end + + test "list metadata function - fallback to callback" do + buffer = """ + defmodule MyLocalModule do + @behaviour ElixirSenseExample.BehaviourWithMeta + + @impl true + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flat + end + end + """ + + list = + Suggestion.suggestions(buffer, 12, 23) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{ + args: "list", + arity: 1, + def_arity: 1, + metadata: %{implementing: ElixirSenseExample.BehaviourWithMeta}, + name: "flatten", + origin: "MyLocalModule", + spec: "@callback flatten(list()) :: list()", + summary: "Sample doc", + type: :function, + visibility: :public + } + ] = list + end + + test "list metadata function - fallback to erlang callback" do + buffer = """ + defmodule MyLocalModule do + @behaviour :gen_statem + + @impl true + def init(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.ini + end + end + """ + + list = + Suggestion.suggestions(buffer, 12, 22) + |> Enum.filter(fn s -> s.type == :function end) + + if System.otp_release() |> String.to_integer() >= 23 do + assert [ + %{ + args: "list", + arity: 1, + def_arity: 1, + metadata: %{implementing: :gen_statem, since: "OTP 19.0"}, + name: "init", + origin: "MyLocalModule", + spec: "@callback init(args :: term()) ::" <> _, + summary: "- Args = term" <> _, + type: :function, + visibility: :public + } + ] = list + end + end + + test "list metadata macro - fallback to macrocallback" do + buffer = """ + defmodule MyLocalModule do + @behaviour ElixirSenseExample.BehaviourWithMeta + + @impl true + defmacro bar(list) do + [] + end + end + + defmodule MyModule do + require MyLocalModule + def func(list) do + MyLocalModule.ba + end + end + """ + + list = + Suggestion.suggestions(buffer, 13, 21) + |> Enum.filter(fn s -> s.type == :macro end) + + assert [ + %{ + args: "list", + arity: 1, + def_arity: 1, + metadata: %{implementing: ElixirSenseExample.BehaviourWithMeta}, + name: "bar", + origin: "MyLocalModule", + spec: "@macrocallback bar(integer()) :: Macro.t()", + summary: "Docs for bar", + type: :macro, + visibility: :public + } + ] = list + end + + test "lists callbacks in function suggestion - elixir behaviour" do + buffer = """ + defmodule MyServer do + use GenServer + + def handle_call(request, _from, state) do + term + end + + def init(arg), do: arg + + def handle_cast(arg, _state) when is_atom(arg) do + :ok + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 9) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{ + args: "_reason, _state", + arity: 2, + def_arity: 2, + metadata: %{implementing: GenServer}, + name: "terminate", + origin: "MyServer", + spec: "@callback terminate(reason, state :: term()) :: term()" <> _, + summary: + "Invoked when the server is about to exit. It should do any cleanup required.", + type: :function, + visibility: :public + } + ] = list + end + + test "lists callbacks in function suggestion - erlang behaviour" do + buffer = """ + defmodule MyServer do + @behaviour :gen_event + + def handle_call(request, _from, state) do + ini + end + + def init(arg), do: arg + + def handle_cast(arg, _state) when is_atom(arg) do + :ok + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 8) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{name: "init", origin: "MyServer", arity: 1} = init_res, + %{name: "is_function", origin: "Kernel", arity: 1}, + %{name: "is_function", origin: "Kernel", arity: 2} + ] = list + + if System.otp_release() |> String.to_integer() >= 23 do + assert %{ + summary: "- InitArgs = Args" <> _, + metadata: %{implementing: :gen_event}, + spec: "@callback init(initArgs :: term()) ::" <> _, + args_list: ["arg"] + } = init_res + end + end + + test "lists fuzzy callbacks in function suggestion - erlang behaviour" do + buffer = """ + defmodule MyServer do + @behaviour :gen_server + + def handle_call(request, _from, state) do + iit + end + + def init(arg), do: arg + + def handle_cast(arg, _state) when is_atom(arg) do + :ok + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 8) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{name: "init", origin: "MyServer", arity: 1}, + %{name: "is_bitstring", origin: "Kernel", arity: 1}, + %{name: "is_integer", origin: "Kernel", arity: 1}, + %{name: "is_list", origin: "Kernel", arity: 1} + ] = list + end + + test "suggest elixir behaviour callbacks on implementation" do + buffer = """ + ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl.ba + """ + + list = + Suggestion.suggestions(buffer, 1, 57) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{ + args: "a", + args_list: ["a"], + arity: 1, + def_arity: 1, + metadata: %{implementing: ElixirSenseExample.ExampleBehaviourWithDoc}, + name: "baz", + origin: "ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl", + snippet: nil, + spec: "@callback baz(integer()) :: :ok", + summary: "Docs for baz", + type: :function, + visibility: :public + } + ] = list + end + + test "suggest erlang behaviour callbacks on implementation" do + buffer = """ + ElixirSenseExample.ExampleBehaviourWithDocCallbackErlang.ini + """ + + list = + Suggestion.suggestions(buffer, 1, 60) + |> Enum.filter(fn s -> s.type == :function end) + + if System.otp_release() |> String.to_integer() >= 23 do + assert [ + %{ + args: "_", + args_list: ["_"], + arity: 1, + def_arity: 1, + metadata: %{implementing: :gen_statem}, + name: "init", + origin: "ElixirSenseExample.ExampleBehaviourWithDocCallbackErlang", + snippet: nil, + spec: "@callback init(args :: term()) :: init_result(state())", + summary: "- Args = term" <> _, + type: :function, + visibility: :public + } + ] = list + end + end + + if System.otp_release() |> String.to_integer() >= 25 do + test "suggest erlang behaviour callbacks on erlang implementation" do + buffer = """ + :file_server.ini + """ + + list = + Suggestion.suggestions(buffer, 1, 17) + |> Enum.filter(fn s -> s.type == :function end) + + assert [ + %{ + args: "args", + args_list: ["args"], + arity: 1, + def_arity: 1, + metadata: %{implementing: :gen_server}, + name: "init", + origin: ":file_server", + snippet: nil, + spec: "@callback init(args :: term()) ::" <> _, + summary: "- Args = term" <> _, + type: :function, + visibility: :public + } + ] = list + end + end + + test "lists params and vars" do + buffer = """ + defmodule MyServer do + use GenServer + + def handle_call(request, _from, state) do + var1 = true + + end + + def init(arg), do: arg + + def handle_cast(arg, _state) when is_atom(arg) do + :ok + end + end + """ + + list = + Suggestion.suggestions(buffer, 6, 5) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "request", type: :variable}, + %{name: "state", type: :variable}, + %{name: "var1", type: :variable} + ] + + list = + Suggestion.suggestions(buffer, 9, 22) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "arg", type: :variable} + ] + + list = + Suggestion.suggestions(buffer, 11, 45) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "arg", type: :variable} + ] + end + + test "lists params in fn's" do + buffer = """ + defmodule MyServer do + my = fn arg -> arg + 1 end + end + """ + + list = + Suggestion.suggestions(buffer, 2, 19) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "arg", type: :variable} + ] + end + + test "lists params in protocol implementations" do + buffer = """ + defimpl Enum, for: [MyStruct, MyOtherStruct] do + def count(term), do: + end + """ + + list = + Suggestion.suggestions(buffer, 2, 24) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "term", type: :variable} + ] + end + + test "lists vars in []" do + buffer = """ + defmodule MyServer do + my = %{} + x = 4 + my[] + + end + """ + + list = + Suggestion.suggestions(buffer, 4, 6) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "my", type: :variable}, + %{name: "x", type: :variable} + ] + end + + test "lists vars in unfinished []" do + buffer = """ + defmodule MyServer do + my = %{} + x = 4 + my[ + + end + """ + + list = + Suggestion.suggestions(buffer, 4, 6) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "my", type: :variable}, + %{name: "x", type: :variable} + ] + end + + test "lists vars in unfinished fn" do + buffer = """ + defmodule MyServer do + [] + |> Enum.min_by(fn x -> + end + """ + + list = + Suggestion.suggestions(buffer, 3, 26) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + end + + test "lists vars in string interpolation" do + buffer = """ + defmodule MyServer do + x = 4 + "abc\#{}" + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 9) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + end + + test "lists vars in unfinished string interpolation" do + buffer = """ + defmodule MyServer do + x = 4 + "abc\#{ + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 9) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + + buffer = """ + defmodule MyServer do + x = 4 + "abc\#{" + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 9) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + + buffer = """ + defmodule MyServer do + x = 4 + "abc\#{} + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 9) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + + buffer = """ + defmodule MyServer do + x = 4 + "abc\#{x[ + + end + """ + + list = + Suggestion.suggestions(buffer, 3, 9) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + end + + test "lists vars in heredoc interpolation" do + buffer = """ + defmodule MyServer do + x = 4 + \"\"\" + abc\#{} + \"\"\" + + end + """ + + list = + Suggestion.suggestions(buffer, 4, 8) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + end + + test "lists vars in unfinished heredoc interpolation" do + buffer = """ + defmodule MyServer do + x = 4 + \"\"\" + abc\#{ + \"\"\" + + end + """ + + list = + Suggestion.suggestions(buffer, 4, 8) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + + buffer = """ + defmodule MyServer do + x = 4 + \"\"\" + abc\#{ + + end + """ + + list = + Suggestion.suggestions(buffer, 4, 8) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + + buffer = """ + defmodule MyServer do + x = 4 + \"\"\" + abc\#{} + + end + """ + + list = + Suggestion.suggestions(buffer, 4, 8) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "x", type: :variable} + ] + end + + test "lists params in fn's not finished multiline" do + buffer = """ + defmodule MyServer do + my = fn arg -> + + end + """ + + assert capture_io(:stderr, fn -> + list = + Suggestion.suggestions(buffer, 3, 5) + |> Enum.filter(fn s -> s.type == :variable end) + + send(self(), {:result, list}) + end) =~ "an expression is always required on the right side of ->" + + assert_received {:result, list} + + assert list == [%{name: "arg", type: :variable}] + end + + test "lists params in fn's not finished" do + buffer = """ + defmodule MyServer do + my = fn arg -> + end + """ + + assert capture_io(:stderr, fn -> + list = + Suggestion.suggestions(buffer, 2, 19) + |> Enum.filter(fn s -> s.type == :variable end) + + send(self(), {:result, list}) + end) =~ "an expression is always required on the right side of ->" + + assert_received {:result, list} + + assert list == [ + %{name: "arg", type: :variable}, + # FIXME my is not defined, should not be in the list + %{name: "my", type: :variable} + ] + end + + test "lists params in defs not finished" do + buffer = """ + defmodule MyServer do + def my(arg), do: + end + """ + + list = + Suggestion.suggestions(buffer, 2, 20) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "arg", type: :variable} + ] + end + + test "lists params and vars in case clauses" do + buffer = """ + defmodule MyServer do + def fun(request) do + case request do + {:atom1, vara} -> + :ok + {:atom2, varb} -> :ok + abc when is_atom(a) + end + + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 9) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "request", type: :variable}, + %{name: "vara", type: :variable} + ] + + list = + Suggestion.suggestions(buffer, 6, 25) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "request", type: :variable}, + %{name: "varb", type: :variable} + ] + + list = + Suggestion.suggestions(buffer, 9, 4) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "request", type: :variable} + ] + + list = + Suggestion.suggestions(buffer, 7, 25) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "abc", type: :variable} + ] + end + + test "lists params and vars in cond clauses" do + buffer = """ + defmodule MyServer do + def fun(request) do + cond do + vara = Enum.find(request, 4) -> + :ok + varb = Enum.find(request, 5) -> :ok + true -> :error + end + + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 9) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "request", type: :variable}, + %{name: "vara", type: :variable} + ] + + list = + Suggestion.suggestions(buffer, 6, 39) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "request", type: :variable}, + %{name: "varb", type: :variable} + ] + + list = + Suggestion.suggestions(buffer, 9, 4) + |> Enum.filter(fn s -> s.type == :variable end) + + assert list == [ + %{name: "request", type: :variable} + ] + end + + test "only list defined params in guard" do + buffer = """ + defmodule MyServer do + def new(my_var) when is_integer(my + end + """ + + list = + Suggestion.suggestions(buffer, 2, 37) + |> Enum.filter(fn s -> s.type in [:variable] end) + + assert list == [%{name: "my_var", type: :variable}] + end + + test "list vars in multiline struct" do + buffer = """ + defmodule MyServer do + def go do + %Some{ + filed: my_var, + other: my + } = abc() + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 16) + |> Enum.filter(fn s -> s.type in [:variable] end) + + assert list == [%{name: "my_var", type: :variable}] + end + + test "tuple destructuring" do + buffer = """ + defmodule MyServer do + def new() do + case NaiveDateTime.new(1, 2) do + {:ok, x} -> x.h + end + case NaiveDateTime.new(1, 2) do + {:ok, x} -> %{x | h} + end + end + end + """ + + list = + Suggestion.suggestions(buffer, 4, 22) + |> Enum.filter(fn s -> s.type == :field end) + + assert [%{name: "hour", origin: "NaiveDateTime"}] = list + + list = + Suggestion.suggestions(buffer, 7, 26) + |> Enum.filter(fn s -> s.type == :field end) + + assert [%{name: "hour", origin: "NaiveDateTime"}] = list + end + + test "nested binding" do + buffer = """ + defmodule State do + defstruct [formatted: nil] + def new(socket) do + %State{formatted: formatted} = state = socket.assigns.state + state.for + state = %{state | form} + end + end + """ + + list = + Suggestion.suggestions(buffer, 5, 14) + |> Enum.filter(fn s -> s.type == :field end) + + assert [%{name: "formatted", origin: "State"}] = list + + list = + Suggestion.suggestions(buffer, 6, 27) + |> Enum.filter(fn s -> s.type == :field end) + + assert [%{name: "formatted", origin: "State"}] = list + end + + test "variable shadowing function" do + buffer = """ + defmodule Mod do + def my_fun(), do: :ok + def some() do + my_fun = 1 + my_f + end + end + """ + + assert [ + %{name: "my_fun", type: :variable}, + %{name: "my_fun", type: :function} + ] = Suggestion.suggestions(buffer, 5, 9) + end + + describe "suggestions for module attributes" do + test "lists attributes" do + buffer = """ + defmodule MyModule do + @my_attribute1 true + @my_attribute2 false + @ + end + """ + + list = + Suggestion.suggestions(buffer, 4, 4) + |> Enum.filter(fn s -> s.type == :attribute and s.name |> String.starts_with?("@my") end) + |> Enum.map(fn %{name: name} -> name end) + + assert list == ["@my_attribute1", "@my_attribute2"] + end + + test "lists module attributes in module scope" do + buffer = """ + defmodule MyModule do + @myattr "asd" + @moduledoc "asdf" + def some do + @m + end + end + """ + + list = + Suggestion.suggestions(buffer, 2, 5) + |> Enum.filter(fn s -> s.type == :attribute end) + |> Enum.map(fn %{name: name} -> name end) + + assert list == ["@macrocallback", "@moduledoc", "@myattr"] + + list = + Suggestion.suggestions(buffer, 5, 7) + |> Enum.filter(fn s -> s.type == :attribute end) + |> Enum.map(fn %{name: name} -> name end) + + assert list == ["@myattr"] + end + + test "built-in attributes should include documentation" do + buffer = """ + defmodule MyModule do + @call + @enfor + end + """ + + list = + Suggestion.suggestions(buffer, 2, 7) + |> Enum.filter(fn s -> s.type == :attribute end) + + assert [%{summary: "Provides a specification for a behaviour callback."}] = list + + list = + Suggestion.suggestions(buffer, 3, 8) + |> Enum.filter(fn s -> s.type == :attribute end) + + assert [ + %{ + summary: + "Ensures the given keys are always set when building the struct defined in the current module." + } + ] = list + end + + test "non built-in attributes should not include documentation" do + buffer = """ + defmodule MyModule do + @myattr "asd" + def some do + @m + end + end + """ + + list = + Suggestion.suggestions(buffer, 4, 6) + |> Enum.filter(fn s -> s.type == :attribute end) + + assert [%{summary: nil}] = list + end + end + + test "lists builtin module attributes on incomplete code" do + buffer = """ + defmodule My do + def start_link(id) do + GenServer.start_link(__MODULE__, id, name: via_tuple(id)) + end + + @ + def init(id) do + {:ok, + %Some.Mod{ + id: id, + events: [], + version: 0 + }} + end + end + """ + + list = + Suggestion.suggestions(buffer, 6, 4) + |> Enum.filter(fn s -> s.type == :attribute end) + + assert Enum.any?(list, &(&1.name == "@impl")) + assert Enum.any?(list, &(&1.name == "@spec")) + end + + test "do not suggest @@" do + buffer = """ + defmodule MyModule do + @ + @my_attribute1 true + end + """ + + list = + Suggestion.suggestions(buffer, 2, 4) + |> Enum.filter(fn s -> s.type == :attribute end) + |> Enum.map(fn %{name: name} -> name end) + + refute "@@" in list + end + + test "lists doc snippets in module body" do + buffer = """ + defmodule MyModule do + @ + #^ + + @m + # ^ + + def some do + @m + # ^ + end + end + """ + + [cursor_1, cursor_2, cursor_3] = cursors(buffer) + + list = suggestions_by_kind(buffer, cursor_1, :snippet) + + assert [ + %{label: ~s(@doc """"""), detail: detail, documentation: doc}, + %{label: ~s(@moduledoc """""")}, + %{label: ~s(@typedoc """""")}, + %{label: "@doc false"}, + %{label: "@moduledoc false"}, + %{label: "@typedoc false"} + ] = list + + assert detail == "module attribute snippet" + assert doc == "Documents a function/macro/callback" + + list = suggestions_by_kind(buffer, cursor_2, :snippet) + assert [%{label: ~S(@moduledoc """""")}, %{label: "@moduledoc false"}] = list + + assert suggestions_by_kind(buffer, cursor_3, :snippet) == [] + end + + test "fuzzy suggestions for doc snippets" do + buffer = """ + defmodule MyModule do + @tydo + # ^ + end + """ + + list = Suggestion.suggestions(buffer, 2, 7) + + assert [ + %{label: ~s(@typedoc """""")}, + %{label: "@typedoc false"} + ] = list |> Enum.filter(&(&1.type == :generic and &1.kind == :snippet)) + end + + test "functions defined in the module" do + buffer = """ + defmodule ElixirSenseExample.ModuleA do + def test_fun_pub(a), do: :ok + defp test_fun_priv(), do: :ok + defp is_boo_overlaps_kernel(), do: :ok + defdelegate delegate_defined, to: Kernel, as: :is_binary + defdelegate delegate_not_defined, to: Dummy, as: :hello + defguard my_guard_pub(value) when is_integer(value) and rem(value, 2) == 0 + defguardp my_guard_priv(value) when is_integer(value) + defmacro a_macro(a) do + quote do: :ok + end + defmacrop a_macro_priv(a) do + quote do: :ok + end + + def some_fun() do + test + a = &test_fun_pr + is_bo + delegate_ + my_ + a_m + end + end + """ + + assert [ + %{ + arity: 0, + name: "test_fun_priv", + origin: "ElixirSenseExample.ModuleA", + type: :function, + visibility: :private + }, + %{ + arity: 1, + name: "test_fun_pub", + origin: "ElixirSenseExample.ModuleA", + type: :function, + visibility: :public + } + ] = Suggestion.suggestions(buffer, 17, 9) + + assert [ + %{ + arity: 0, + name: "test_fun_priv", + origin: "ElixirSenseExample.ModuleA", + type: :function + } + ] = Suggestion.suggestions(buffer, 18, 21) + + assert [ + %{ + arity: 0, + name: "is_boo_overlaps_kernel", + origin: "ElixirSenseExample.ModuleA", + type: :function + }, + %{ + arity: 1, + name: "is_boolean", + origin: "Kernel", + type: :function + } + ] = Suggestion.suggestions(buffer, 19, 10) + + assert [ + %{ + arity: 0, + name: "delegate_defined", + origin: "ElixirSenseExample.ModuleA", + type: :function + }, + %{ + arity: 0, + name: "delegate_not_defined", + origin: "ElixirSenseExample.ModuleA", + type: :function + } + ] = Suggestion.suggestions(buffer, 20, 14) + + assert [ + %{ + args: "value", + arity: 1, + name: "my_guard_priv", + origin: "ElixirSenseExample.ModuleA", + spec: "", + summary: "", + type: :macro, + visibility: :private + }, + %{ + args: "value", + arity: 1, + name: "my_guard_pub", + origin: "ElixirSenseExample.ModuleA", + spec: "", + summary: "", + type: :macro + } + ] = Suggestion.suggestions(buffer, 21, 8) + + assert [ + %{ + args: "a", + arity: 1, + name: "a_macro", + origin: "ElixirSenseExample.ModuleA", + spec: "", + summary: "", + type: :macro, + visibility: :public + }, + %{ + args: "a", + arity: 1, + name: "a_macro_priv", + origin: "ElixirSenseExample.ModuleA", + spec: "", + summary: "", + type: :macro + } + ] = Suggestion.suggestions(buffer, 22, 8) + end + + test "suggest local macro" do + buffer = """ + defmodule MyModule do + defmacrop some_macro(var), do: Macro.expand(var, __CALLER__) + + defmacro other do + some_ma + end + end + """ + + assert [%{name: "some_macro"}] = Suggestion.suggestions(buffer, 5, 12) + end + + test "does not suggest local macro if it's defined after the cursor" do + buffer = """ + defmodule MyModule do + defmacro other do + some_ma + end + + defmacrop some_macro(var), do: Macro.expand(var, __CALLER__) + end + """ + + assert [] == Suggestion.suggestions(buffer, 3, 12) + end + + test "suggest local function even if it's defined after the cursor" do + buffer = """ + defmodule MyModule do + def other do + some_fu + end + + defp some_fun(var), do: :ok + end + """ + + assert [%{name: "some_fun"}] = Suggestion.suggestions(buffer, 3, 12) + end + + test "functions defined in other module fully qualified" do + buffer = """ + defmodule ElixirSenseExample.ModuleO do + def test_fun_pub(a), do: :ok + defp test_fun_priv(), do: :ok + end + + defmodule ElixirSenseExample.ModuleA do + def some_fun() do + ElixirSenseExample.ModuleO.te + end + end + """ + + assert [ + %{ + arity: 1, + name: "test_fun_pub", + origin: "ElixirSenseExample.ModuleO", + type: :function + } + ] = Suggestion.suggestions(buffer, 8, 34) + end + + test "functions defined in other module aliased" do + buffer = """ + defmodule ElixirSenseExample.ModuleO do + def test_fun_pub(a), do: :ok + defp test_fun_priv(), do: :ok + end + + defmodule ElixirSenseExample.ModuleA do + alias ElixirSenseExample.ModuleO + def some_fun() do + ModuleO.te + end + end + """ + + assert [ + %{ + arity: 1, + name: "test_fun_pub", + origin: "ElixirSenseExample.ModuleO", + type: :function + } + ] = Suggestion.suggestions(buffer, 9, 15) + end + + test "functions defined in other module imported" do + buffer = """ + defmodule ElixirSenseExample.ModuleO do + @spec test_fun_pub(integer) :: atom + def test_fun_pub(a), do: :ok + defp test_fun_priv(), do: :ok + end + + defmodule ElixirSenseExample.ModuleA do + import ElixirSenseExample.ModuleO + def some_fun() do + test + __info + end + end + """ + + assert [ + %{ + arity: 1, + def_arity: 1, + name: "test_fun_pub", + origin: "ElixirSenseExample.ModuleO", + type: :function, + args: "a", + args_list: ["a"], + spec: "@spec test_fun_pub(integer) :: atom", + summary: "", + metadata: %{}, + snippet: nil, + visibility: :public + } + ] = Suggestion.suggestions(buffer, 10, 9) + + # builtin functions not called locally + assert [] == Suggestion.suggestions(buffer, 11, 11) + end + + test "built-in functions not returned on local calls" do + buffer = """ + defmodule ElixirSenseExample.ModuleO do + + end + """ + + refute Enum.any?(Suggestion.suggestions(buffer, 2, 2), &(&1[:name] == "module_info")) + end + + test "built-in functions not returned on remote calls" do + buffer = """ + defmodule ElixirSenseExample.ModuleO do + ElixirSenseExample.ModuleO. + end + """ + + assert Enum.any?(Suggestion.suggestions(buffer, 2, 30), &(&1[:name] == "module_info")) + end + + test "functions and module suggestions with __MODULE__" do + buffer = """ + defmodule ElixirSenseExample.SmodO do + def test_fun_pub(a), do: :ok + defp test_fun_priv(), do: :ok + end + + defmodule ElixirSenseExample do + defp test_fun_priv1(a), do: :ok + def some_fun() do + __MODULE__.Sm + __MODULE__.SmodO.te + __MODULE__.te + __MODULE__.__in + end + end + """ + + assert [ + %{ + name: "SmodO", + type: :module + } + ] = + Suggestion.suggestions(buffer, 9, 18) + |> Enum.filter(&(&1.name |> String.starts_with?("Smo"))) + + assert [ + %{ + arity: 1, + name: "test_fun_pub", + origin: "ElixirSenseExample.SmodO", + type: :function + } + ] = Suggestion.suggestions(buffer, 10, 24) + + # no private on external call + assert [] = Suggestion.suggestions(buffer, 11, 18) + + assert [ + %{ + arity: 1, + name: "__info__", + origin: "ElixirSenseExample", + type: :function + } + ] = Suggestion.suggestions(buffer, 12, 20) + end + + test "Elixir module" do + buffer = """ + defmodule MyModule do + El + end + """ + + list = Suggestion.suggestions(buffer, 2, 5) + + assert %{ + type: :module, + name: "Elixir", + full_name: "Elixir", + subtype: :alias, + summary: "", + metadata: %{} + } = Enum.at(list, 0) + end + + test "suggestion for aliases modules defined by require clause" do + buffer = """ + defmodule Mod do + require Integer, as: I + I.is_o + end + """ + + list = Suggestion.suggestions(buffer, 3, 9) + assert Enum.at(list, 0).name == "is_odd" + end + + test "suggestion for struct fields" do + buffer = """ + defmodule Mod do + %ElixirSenseExample.IO.Stream{} + %ArgumentError{} + end + """ + + list = + Suggestion.suggestions(buffer, 2, 33) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__struct__", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "ElixirSenseExample.IO.Stream" + }, + %{ + name: "device", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "IO.device()" + }, + %{ + name: "line_or_bytes", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: ":line | non_neg_integer()" + }, + %{ + name: "raw", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "boolean()" + } + ] + + list = + Suggestion.suggestions(buffer, 3, 18) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__exception__", + origin: "ArgumentError", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "true" + }, + %{ + name: "__struct__", + origin: "ArgumentError", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "ArgumentError" + }, + %{ + name: "message", + origin: "ArgumentError", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + } + ] + end + + test "suggestion for aliased struct fields" do + buffer = """ + defmodule Mod do + alias ElixirSenseExample.IO.Stream + %Stream{ + end + """ + + list = + Suggestion.suggestions(buffer, 3, 11) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__struct__", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "ElixirSenseExample.IO.Stream" + }, + %{ + name: "device", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "IO.device()" + }, + %{ + name: "line_or_bytes", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: ":line | non_neg_integer()" + }, + %{ + name: "raw", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "boolean()" + } + ] + end + + test "suggestion for builtin fields in struct pattern match" do + buffer = """ + defmodule Mod do + def my(%_{}), do: :ok + def my(%var{}), do: var + end + """ + + list = + Suggestion.suggestions(buffer, 2, 13) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__struct__", + origin: nil, + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "atom()" + } + ] + + list = + Suggestion.suggestions(buffer, 3, 15) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__struct__", + origin: nil, + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "atom()" + } + ] + end + + test "suggestion for aliased struct fields atom module" do + buffer = """ + defmodule Mod do + alias ElixirSenseExample.IO.Stream + %:"Elixir.Stream"{ + end + """ + + list = + Suggestion.suggestions(buffer, 3, 21) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__struct__", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "ElixirSenseExample.IO.Stream" + }, + %{ + name: "device", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "IO.device()" + }, + %{ + name: "line_or_bytes", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: ":line | non_neg_integer()" + }, + %{ + name: "raw", + origin: "ElixirSenseExample.IO.Stream", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "boolean()" + } + ] + end + + test "suggestion for metadata struct fields" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + field_2: "" + ] + + def func do + %MyServer{} + %MyServer{field_2: "2", } + end + end + """ + + list = + Suggestion.suggestions(buffer, 8, 15) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__struct__", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "MyServer" + }, + %{ + name: "field_1", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + }, + %{ + name: "field_2", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + } + ] + + list = Suggestion.suggestions(buffer, 9, 28) + + assert list == [ + %{ + name: "__struct__", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "MyServer" + }, + %{ + name: "field_1", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + } + ] + end + + test "suggestion for metadata struct fields atom module" do + buffer = """ + defmodule :my_server do + defstruct [ + field_1: nil, + field_2: "" + ] + + def func do + %:my_server{} + %:my_server{field_2: "2", } + end + end + """ + + list = + Suggestion.suggestions(buffer, 8, 17) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "__struct__", + origin: ":my_server", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: ":my_server" + }, + %{ + name: "field_1", + origin: ":my_server", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + }, + %{ + name: "field_2", + origin: ":my_server", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + } + ] + + list = Suggestion.suggestions(buffer, 9, 30) + + assert list == [ + %{ + name: "__struct__", + origin: ":my_server", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: ":my_server" + }, + %{ + name: "field_1", + origin: ":my_server", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + } + ] + end + + test "suggestion for metadata struct fields multiline" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + field_2: "" + ] + + def func do + %MyServer{ + field_2: "2", + + } + end + end + """ + + list = Suggestion.suggestions(buffer, 10, 7) + + assert list == [ + %{ + name: "__struct__", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "MyServer" + }, + %{ + name: "field_1", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + } + ] + end + + test "suggestion for metadata struct fields when using `__MODULE__`" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + field_2: "" + ] + + def func do + %__MODULE__{field_2: "2", } + end + end + """ + + list = Suggestion.suggestions(buffer, 8, 31) + + assert list == [ + %{ + name: "__struct__", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: "MyServer" + }, + %{ + name: "field_1", + origin: "MyServer", + type: :field, + call?: false, + subtype: :struct_field, + type_spec: nil + } + ] + end + + test "suggestion for struct fields in variable.key call syntax" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + field_2: "" + ] + + def func do + var_1 = %MyServer{} + var_1.f + end + end + """ + + list = + Suggestion.suggestions(buffer, 9, 12) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "field_1", + origin: "MyServer", + type: :field, + call?: true, + subtype: :struct_field, + type_spec: nil + }, + %{ + name: "field_2", + origin: "MyServer", + type: :field, + call?: true, + subtype: :struct_field, + type_spec: nil + } + ] + end + + test "suggestion for map fields in variable.key call syntax" do + buffer = """ + defmodule MyServer do + def func do + var_1 = %{key_1: 1, key_2: %{abc: 123}} + var_1.k + end + end + """ + + list = + Suggestion.suggestions(buffer, 4, 12) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "key_1", + origin: nil, + type: :field, + call?: true, + subtype: :map_key, + type_spec: nil + }, + %{ + name: "key_2", + origin: nil, + type: :field, + call?: true, + subtype: :map_key, + type_spec: nil + } + ] + end + + test "suggestion for map fields in @attribute.key call syntax" do + buffer = """ + defmodule MyServer do + @var_1 %{key_1: 1, key_2: %{abc: 123}} + def func do + @var_1.k + end + end + """ + + list = + Suggestion.suggestions(buffer, 4, 13) + |> Enum.filter(&(&1.type in [:field])) + + assert list == [ + %{ + name: "key_1", + origin: nil, + type: :field, + call?: true, + subtype: :map_key, + type_spec: nil + }, + %{ + name: "key_2", + origin: nil, + type: :field, + call?: true, + subtype: :map_key, + type_spec: nil + } + ] + end + + test "suggestion for functions in variable.key call syntax" do + buffer = """ + defmodule MyServer do + def func do + var_1 = Atom + var_1.to_str + end + end + """ + + list = + Suggestion.suggestions(buffer, 4, 17) + |> Enum.filter(&(&1.type in [:function])) + + assert [%{name: "to_string", origin: "Atom", type: :function}] = list + end + + test "suggestion for vars in struct update" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + some_field: "" + ] + + def some_func() do + false + end + + def func(%MyServer{} = some_arg) do + %MyServer{some + end + end + """ + + list = Suggestion.suggestions(buffer, 12, 19) + + assert [ + %{ + origin: "MyServer", + type: :field, + name: "some_field", + call?: false, + subtype: :struct_field + }, + %{name: "some_arg", type: :variable}, + %{name: "some_func", type: :function} + ] = list + end + + test "suggestion for fields in struct update" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + some_field: "" + ] + + def func(%MyServer{} = some_arg) do + %MyServer{some_arg | fiel + end + end + """ + + list = Suggestion.suggestions(buffer, 8, 30) + + assert list == [ + %{ + call?: false, + name: "field_1", + origin: "MyServer", + subtype: :struct_field, + type: :field, + type_spec: nil + } + ] + end + + test "suggestion for fields in struct update variable when module not set" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + some_field: "" + ] + + def func(%MyServer{} = some_arg) do + %{some_arg | fiel + end + end + """ + + list = Suggestion.suggestions(buffer, 8, 22) + + assert list == [ + %{ + call?: false, + name: "field_1", + origin: "MyServer", + subtype: :struct_field, + type: :field, + type_spec: nil + } + ] + end + + test "suggestion for fields in struct update attribute when module not set" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + some_field: "" + ] + + @str %MyServer{} + + %{@str | fiel + end + """ + + list = Suggestion.suggestions(buffer, 9, 16) + + assert list == [ + %{ + call?: false, + name: "field_1", + origin: "MyServer", + subtype: :struct_field, + type: :field, + type_spec: nil + } + ] + end + + test "suggestion for fields in struct update when struct type is var" do + buffer = """ + defmodule MyServer do + def func(%var{field_1: "asd"} = some_arg) do + %{some_arg | fiel + end + end + """ + + list = Suggestion.suggestions(buffer, 3, 22) + + assert list == [ + %{ + call?: false, + name: "field_1", + origin: nil, + subtype: :struct_field, + type: :field, + type_spec: nil + } + ] + end + + test "suggestion for fields in struct when struct type is attribute" do + buffer = """ + defmodule MyServer do + @t Time + %@t{ho + end + """ + + list = Suggestion.suggestions(buffer, 3, 9) + + assert list == [ + %{ + call?: false, + name: "hour", + origin: "Time", + subtype: :struct_field, + type: :field, + type_spec: "Calendar.hour()" + } + ] + end + + test "suggestion for keys in map update" do + buffer = """ + defmodule MyServer do + def func(%{field_1: "asd"} = some_arg) do + %{some_arg | fiel + end + end + """ + + list = Suggestion.suggestions(buffer, 3, 22) + + assert list == [ + %{ + call?: false, + name: "field_1", + origin: nil, + subtype: :map_key, + type: :field, + type_spec: nil + } + ] + end + + test "suggestion for fuzzy struct fields" do + buffer = """ + defmodule MyServer do + def func(%{field_1: "asd"} = some_arg) do + %{some_arg | fie1 + end + end + """ + + list = Suggestion.suggestions(buffer, 3, 22) + + assert list == [ + %{ + call?: false, + name: "field_1", + origin: nil, + subtype: :map_key, + type: :field, + type_spec: nil + } + ] + end + + test "suggestion for funcs and vars in struct" do + buffer = """ + defmodule MyServer do + defstruct [ + field_1: nil, + some_field: "" + ] + + def other_func(), do: :ok + + def func(%MyServer{} = some_arg, other_arg) do + %MyServer{some_arg | + field_1: ot + end + end + """ + + list = Suggestion.suggestions(buffer, 11, 18) + + assert [ + %{name: "other_arg", type: :variable}, + %{ + name: "other_func", + type: :function, + args: "", + args_list: [], + arity: 0, + def_arity: 0, + origin: "MyServer", + spec: "", + summary: "", + visibility: :public, + snippet: nil, + metadata: %{} + } + ] = list + end + + test "no suggestion of fields when the module is not a struct" do + buffer = """ + defmodule Mod do + %Enum{ + end + """ + + list = Suggestion.suggestions(buffer, 2, 9) + assert Enum.any?(list, fn %{type: type} -> type == :field end) == false + end + + test "suggest struct fields when metadata function evaluates to struct" do + buffer = """ + defmodule Mod do + defstruct [field: nil] + @type t :: %__MODULE__{} + + @spec fun() :: t + def fun(), do: %Mod{} + + def some do + var = fun() + var. + end + end + """ + + list = Suggestion.suggestions(buffer, 10, 9) + + assert [ + %{call?: true, name: "__struct__", origin: "Mod"}, + %{call?: true, name: "field", origin: "Mod", subtype: :struct_field, type: :field} + ] = list + end + + test "suggest struct fields when metadata function evaluates to remote type" do + buffer = """ + defmodule Mod do + @spec fun() :: NaiveDateTime.t() + def fun(), do: NaiveDateTime.new(1, 2) + + def some do + var = fun() + var.h + end + end + """ + + list = Suggestion.suggestions(buffer, 7, 10) + + assert [%{name: "hour", origin: "NaiveDateTime"}] = list + end + + test "suggest struct fields when metadata function evaluates to remote type aliased" do + buffer = """ + defmodule Mod do + alias NaiveDateTime, as: MyType + @spec fun() :: MyType.t() + def fun(), do: MyType.new(1, 2) + + def some do + var = fun() + var.h + end + end + """ + + list = Suggestion.suggestions(buffer, 8, 10) + + assert [%{name: "hour", origin: "NaiveDateTime"}] = list + end + + test "suggest struct fields when metadata function evaluates to remote type __MODULE__" do + buffer = """ + defmodule Mod do + @type t :: NaiveDateTime.t() + + @spec fun() :: __MODULE__.t() + def fun(), do: nil + + def some do + var = fun() + var.h + end + end + """ + + list = Suggestion.suggestions(buffer, 9, 10) + + assert [%{name: "hour", origin: "NaiveDateTime"}] = list + end + + test "suggest struct fields when metadata function evaluates to remote type __MODULE__.Submodule" do + buffer = """ + defmodule Mod do + defmodule Sub do + @type t :: NaiveDateTime.t() + end + + @spec fun() :: __MODULE__.Sub.t() + def fun(), do: nil + + def some do + var = fun() + var.h + end + end + """ + + list = Suggestion.suggestions(buffer, 11, 10) + + assert [%{name: "hour", origin: "NaiveDateTime"}] = list + end + + test "suggest struct fields when variable is struct" do + buffer = """ + defmodule Abc do + defstruct [:cde] + end + + defmodule Mod do + def my() do + some(abc) + abc = %Abc{cde: 1} + abc. + end + end + """ + + list = Suggestion.suggestions(buffer, 9, 9) + + assert [ + %{call?: true, name: "__struct__", origin: "Abc"}, + %{call?: true, name: "cde", origin: "Abc", subtype: :struct_field, type: :field} + ] = list + end + + test "suggest struct fields when variable is rebound to struct" do + buffer = """ + defmodule Abc do + defstruct [:cde] + end + + defmodule Mod do + def my() do + abc = 1 + some(abc) + abc = %Abc{cde: 1} + abc.cde + abc = 1 + end + end + """ + + list = Suggestion.suggestions(buffer, 10, 9) + + assert [ + %{call?: true, name: "__struct__", origin: "Abc"}, + %{call?: true, name: "cde", origin: "Abc", subtype: :struct_field, type: :field} + ] = list + end + + test "suggest struct fields when attribute is struct" do + buffer = """ + defmodule Abc do + defstruct [:cde] + end + + defmodule Mod do + @abc %Abc{cde: 1} + @abc. + end + """ + + list = Suggestion.suggestions(buffer, 7, 8) + + assert [ + %{call?: true, name: "__struct__", origin: "Abc"}, + %{call?: true, name: "cde", origin: "Abc", subtype: :struct_field, type: :field} + ] = list + end + + test "suggest struct fields when attribute is rebound to struct" do + buffer = """ + defmodule Abc do + defstruct [:cde] + end + + defmodule Mod do + @abc 1 + @abc %Abc{cde: 1} + @abc. + end + """ + + list = Suggestion.suggestions(buffer, 8, 8) + + assert [ + %{call?: true, name: "__struct__", origin: "Abc"}, + %{call?: true, name: "cde", origin: "Abc", subtype: :struct_field, type: :field} + ] = list + end + + test "suggest modules to alias" do + buffer = """ + defmodule MyModule do + alias Str + end + """ + + list = + Suggestion.suggestions(buffer, 2, 12) + |> Enum.filter(fn s -> s.type == :module end) + + assert [ + %{name: "Stream"}, + %{name: "StreamData"}, + %{name: "String"}, + %{name: "StringIO"} + ] = list |> Enum.filter(&(&1.name |> String.starts_with?("Str"))) + end + + test "suggest modules to alias with __MODULE__" do + buffer = """ + defmodule Stream do + alias __MODULE__.Re + end + """ + + list = Suggestion.suggestions(buffer, 2, 22) + + assert [%{name: "Reducers", type: :module} | _] = list + end + + test "suggest modules to alias in multi alias syntax" do + buffer = """ + defmodule MyModule do + alias Stream.{Re + end + """ + + list = Suggestion.suggestions(buffer, 2, 19) + + assert [%{name: "Reducers", type: :module}] = list + end + + test "suggest modules to alias in multi alias syntax with __MODULE__" do + buffer = """ + defmodule Stream do + alias __MODULE__.{Re + end + """ + + list = Suggestion.suggestions(buffer, 2, 23) + + assert [%{name: "Reducers", type: :module}] = list + end + + describe "suggestion for param options" do + test "suggest more than one option" do + buffer = "Local.func_with_options(" + + list = suggestions_by_type(:param_option, buffer) + assert length(list) > 1 + end + + test "are fuzzy" do + buffer = "Local.func_with_options(remo_wi" + list = suggestions_by_type(:param_option, buffer) + assert [%{name: "remote_with_params_o"}] = list + end + + test "handles macros" do + buffer = """ + require Local + Local.macro_with_options(remo_wi\ + """ + + list = suggestions_by_type(:param_option, buffer) + assert [%{name: "remote_with_params_o"}] = list + end + + test "suggest the same list when options are already set" do + buffer1 = "Local.func_with_options(" + buffer2 = "Local.func_with_options(local_o: :an_atom, " + + capture_io(:stderr, fn -> + result1 = suggestions_by_type(:param_option, buffer1) + result2 = suggestions_by_type(:param_option, buffer2) + send(self(), {:results, result1, result2}) + end) + + assert_received {:results, result1, result2} + assert result1 == result2 + end + + test "options as inline list" do + buffer = "Local.func_with_options_as_inline_list(" + + assert %{type_spec: "local_t()", expanded_spec: "@type local_t() :: atom()"} = + suggestion_by_name("local_o", buffer) + + assert %{ + type_spec: "keyword()", + expanded_spec: """ + @type keyword() :: [ + {atom(), any()} + ]\ + """ + } = suggestion_by_name("builtin_o", buffer) + end + + test "options vars defined in when" do + type_spec = "local_t()" + origin = "ElixirSenseExample.ModuleWithTypespecs.Local" + spec = "@type local_t() :: atom()" + + buffer = "Local.func_with_option_var_defined_in_when(" + suggestion = suggestion_by_name("local_o", buffer) + + assert suggestion.type_spec == type_spec + assert suggestion.origin == origin + assert suggestion.expanded_spec == spec + + buffer = "Local.func_with_options_var_defined_in_when(" + suggestion = suggestion_by_name("local_o", buffer) + + assert suggestion.type_spec == type_spec + assert suggestion.origin == origin + assert suggestion.expanded_spec == spec + end + + test "opaque type internal structure is not revealed" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("opaque_o", buffer) + + assert suggestion.type_spec == "opaque_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + assert suggestion.expanded_spec == "@opaque opaque_t()" + assert suggestion.doc == "Local opaque type" + end + + test "private type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("private_o", buffer) + + assert suggestion.type_spec == "private_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + assert suggestion.expanded_spec == "@typep private_t() :: atom()" + assert suggestion.doc == "" + end + + test "local type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("local_o", buffer) + + assert suggestion.type_spec == "local_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + assert suggestion.expanded_spec == "@type local_t() :: atom()" + assert suggestion.doc == "Local type" + end + + test "local type with params" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("local_with_params_o", buffer) + + assert suggestion.type_spec == "local_t(atom(), integer())" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + assert suggestion.expanded_spec =~ "@type local_t(a, b) ::" + end + + test "basic type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("basic_o", buffer) + + assert suggestion.type_spec == "pid()" + assert suggestion.origin == "" + assert suggestion.expanded_spec == "" + assert suggestion.doc == "A process identifier, pid, identifies a process" + end + + test "basic type with params" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("basic_with_params_o", buffer) + + assert suggestion.type_spec == "[atom(), ...]" + assert suggestion.origin == "" + assert suggestion.expanded_spec == "" + assert suggestion.doc == "Non-empty proper list" + end + + test "built-in type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("builtin_o", buffer) + + assert suggestion.type_spec == "keyword()" + assert suggestion.origin == "" + + assert suggestion.expanded_spec == """ + @type keyword() :: [ + {atom(), any()} + ]\ + """ + + assert suggestion.doc == "A keyword list" + end + + test "built-in type with params" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("builtin_with_params_o", buffer) + + assert suggestion.type_spec == "keyword(term())" + assert suggestion.origin == "" + assert suggestion.expanded_spec =~ "@type keyword(t()) ::" + assert suggestion.doc == "A keyword list with values of type `t`" + end + + test "union type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("union_o", buffer) + + assert suggestion.type_spec == "union_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + + assert suggestion.expanded_spec == """ + @type union_t() :: + atom() | integer()\ + """ + end + + test "list type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("list_o", buffer) + + assert suggestion.type_spec == "list_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + assert suggestion.expanded_spec =~ "@type list_t() ::" + end + + test "remote type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("remote_o", buffer) + + assert suggestion.type_spec == "ElixirSenseExample.ModuleWithTypespecs.Remote.remote_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Remote" + assert suggestion.expanded_spec == "@type remote_t() :: atom()" + assert suggestion.doc == "Remote type" + end + + test "remote type with args" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("remote_with_params_o", buffer) + + assert suggestion.type_spec == + "ElixirSenseExample.ModuleWithTypespecs.Remote.remote_t(atom(), integer())" + + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Remote" + assert suggestion.expanded_spec =~ "@type remote_t(a, b) ::" + assert suggestion.doc == "Remote type with params" + end + + test "remote erlang type with doc" do + buffer = "Local.func_with_erlang_type_options(" + suggestion = suggestion_by_name("erlang_t", buffer) + + assert suggestion.type_spec == + ":erlang.time_unit()" + + assert suggestion.origin == ":erlang" + + assert suggestion.expanded_spec == + "@type time_unit() ::\n pos_integer()\n | :second\n | :millisecond\n | :microsecond\n | :nanosecond\n | :native\n | :perf_counter\n | deprecated_time_unit()" + + if System.otp_release() |> String.to_integer() >= 23 do + assert suggestion.doc =~ "Supported time unit representations" + end + end + + test "remote aliased type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("remote_aliased_o", buffer) + + assert suggestion.type_spec == "remote_aliased_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + + assert suggestion.expanded_spec == """ + @type remote_aliased_t() :: + ElixirSenseExample.ModuleWithTypespecs.Remote.remote_t() + | ElixirSenseExample.ModuleWithTypespecs.Remote.remote_list_t()\ + """ + + assert suggestion.doc == "Remote type from aliased module" + end + + test "remote aliased inline type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("remote_aliased_inline_o", buffer) + + assert suggestion.type_spec == "ElixirSenseExample.ModuleWithTypespecs.Remote.remote_t()" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Remote" + assert suggestion.expanded_spec == "@type remote_t() :: atom()" + assert suggestion.doc == "Remote type" + end + + test "inline list type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("inline_list_o", buffer) + + assert suggestion.type_spec == "[:trace | :log]" + assert suggestion.origin == "" + assert suggestion.expanded_spec == "" + assert suggestion.doc == "" + end + + test "non existent type" do + buffer = "Local.func_with_options(" + suggestion = suggestion_by_name("non_existent_o", buffer) + + assert suggestion.type_spec == + "ElixirSenseExample.ModuleWithTypespecs.Remote.non_existent()" + + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Remote" + assert suggestion.expanded_spec == "" + assert suggestion.doc == "" + end + + test "named options" do + buffer = "Local.func_with_named_options(" + assert suggestion_by_name("local_o", buffer).type_spec == "local_t()" + end + + test "options with only one option" do + buffer = "Local.func_with_one_option(" + assert suggestion_by_name("option_1", buffer).type_spec == "integer()" + end + + test "union of options" do + buffer = "Local.func_with_union_of_options(" + + assert suggestion_by_name("local_o", buffer).type_spec == "local_t()" + assert suggestion_by_name("option_1", buffer).type_spec == "atom()" + end + + test "union of options inline" do + buffer = "Local.func_with_union_of_options_inline(" + + assert suggestion_by_name("local_o", buffer).type_spec == "local_t()" + assert suggestion_by_name("option_1", buffer).type_spec == "atom()" + end + + test "union of options (local and remote) as type + inline" do + buffer = "Local.func_with_union_of_options_as_type(" + assert suggestion_by_name("option_1", buffer).type_spec == "boolean()" + + suggestion = suggestion_by_name("remote_option_1", buffer) + assert suggestion.type_spec == "ElixirSenseExample.ModuleWithTypespecs.Remote.remote_t()" + assert suggestion.expanded_spec == "@type remote_t() :: atom()" + assert suggestion.doc == "Remote type" + end + + test "atom only options" do + buffer = ":ets.new(:name," + + assert suggestion_by_name("duplicate_bag", buffer).type_spec == "" + assert suggestion_by_name("named_table", buffer).doc == "" + end + + test "format type spec" do + buffer = "Local.func_with_options(" + + assert suggestion_by_name("large_o", buffer).expanded_spec == """ + @type large_t() :: + pid() + | port() + | (registered_name :: + atom()) + | {registered_name :: + atom(), node()}\ + """ + end + end + + describe "suggestions for typespecs" do + test "remote types - filter list of typespecs" do + buffer = """ + defmodule My do + @type a :: Remote.remote_t\ + """ + + list = suggestions_by_type(:type_spec, buffer) + assert length(list) == 4 + end + + test "remote types - retrieve info from typespecs" do + buffer = """ + defmodule My do + @type a :: Remote.\ + """ + + suggestion = suggestion_by_name("remote_list_t", buffer) + + assert suggestion.spec == """ + @type remote_list_t() :: [ + remote_t() + ]\ + """ + + assert suggestion.signature == "remote_list_t()" + assert suggestion.arity == 0 + assert suggestion.doc == "Remote list type" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Remote" + end + + test "on specs" do + buffer = """ + defmodule My do + @spec a() :: Remote.\ + """ + + assert %{name: "remote_list_t"} = suggestion_by_name("remote_list_t", buffer) + + buffer = """ + defmodule My do + @spec a(Remote.) :: integer + end + """ + + assert %{name: "remote_list_t"} = suggestion_by_name("remote_list_t", buffer, 2, 18) + + buffer = """ + defmodule My do + @spec a(Remote.) + end + """ + + assert %{name: "remote_list_t"} = suggestion_by_name("remote_list_t", buffer, 2, 18) + end + + test "on callbacks" do + buffer = """ + defmodule My do + @callback a() :: none + end + """ + + assert [_, _] = suggestions_by_name("nonempty_list", buffer, 2, 24) + + buffer = """ + defmodule My do + @callback a(none) :: integer + end + """ + + assert [_, _] = suggestions_by_name("nonempty_list", buffer, 2, 19) + + buffer = """ + defmodule My do + @callback a(none) + end + """ + + assert [_, _] = suggestions_by_name("nonempty_list", buffer, 2, 19) + end + + test "remote types - by attribute" do + buffer = """ + defmodule My do + @type my_type :: integer + @attr My + @type some :: @attr.my\ + """ + + [suggestion_1] = suggestions_by_name("my_type", buffer) + + assert suggestion_1.signature == "my_type()" + end + + test "remote types - by __MODULE__" do + buffer = """ + defmodule My do + @type my_type :: integer + @type some :: __MODULE__.my\ + """ + + [suggestion_1] = suggestions_by_name("my_type", buffer) + + assert suggestion_1.signature == "my_type()" + end + + test "remote types - retrieve info from typespecs with params" do + buffer = """ + defmodule My do + @type a :: Remote.\ + """ + + [suggestion_1, suggestion_2] = suggestions_by_name("remote_t", buffer) + + assert suggestion_1.spec == "@type remote_t() :: atom()" + assert suggestion_1.signature == "remote_t()" + assert suggestion_1.arity == 0 + assert suggestion_1.doc == "Remote type" + assert suggestion_1.origin == "ElixirSenseExample.ModuleWithTypespecs.Remote" + + assert suggestion_2.spec =~ "@type remote_t(a, b) ::" + assert suggestion_2.signature == "remote_t(a, b)" + assert suggestion_2.arity == 2 + assert suggestion_2.doc == "Remote type with params" + assert suggestion_2.origin == "ElixirSenseExample.ModuleWithTypespecs.Remote" + end + + test "local types - filter list of typespecs" do + buffer = """ + defmodule ElixirSenseExample.ModuleWithTypespecs.Local do + # The types are defined in `test/support/module_with_typespecs.ex` + @type my_type :: local_ + # ^ + end + """ + + list = + Suggestion.suggestions(buffer, 3, 26) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + assert length(list) == 2 + end + + test "typespec fuzzy match" do + buffer = """ + defmodule ElixirSenseExample.ModuleWithTypespecs.Local do + # The types are defined in `test/support/module_with_typespecs.ex` + @type fuzzy_type :: loca_ + # ^ + end + """ + + list = + Suggestion.suggestions(buffer, 3, 27) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + [suggestion, _] = list + + assert suggestion.spec == "@type local_t() :: atom()" + assert suggestion.signature == "local_t()" + assert suggestion.arity == 0 + assert suggestion.doc == "Local type" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + end + + test "local types - retrieve info from typespecs" do + buffer = """ + defmodule ElixirSenseExample.ModuleWithTypespecs.Local do + # The types are defined in `test/support/module_with_typespecs.ex` + @type my_type :: local_t + # ^ + end + """ + + list = + Suggestion.suggestions(buffer, 3, 27) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + [suggestion, _] = list + + assert suggestion.spec == "@type local_t() :: atom()" + assert suggestion.signature == "local_t()" + assert suggestion.arity == 0 + assert suggestion.doc == "Local type" + assert suggestion.origin == "ElixirSenseExample.ModuleWithTypespecs.Local" + end + + test "builtin types - filter list of typespecs" do + buffer = "defmodule My, do: @type my_type :: lis" + + list = suggestions_by_type(:type_spec, buffer) + assert length(list) == 2 + end + + test "builtin types - retrieve info from typespecs" do + buffer = "defmodule My, do: @type my_type :: lis" + + [suggestion | _] = suggestions_by_type(:type_spec, buffer) + + assert suggestion.spec == "@type list() :: [any()]" + assert suggestion.signature == "list()" + assert suggestion.arity == 0 + assert suggestion.doc == "A list" + assert suggestion.origin == nil + end + + test "builtin types - retrieve info from typespecs with params" do + buffer = "defmodule My, do: @type my_type :: lis" + + [_, suggestion | _] = suggestions_by_type(:type_spec, buffer) + + assert suggestion.spec == "@type list(t())" + assert suggestion.signature == "list(t())" + assert suggestion.arity == 1 + assert suggestion.doc == "Proper list ([]-terminated)" + assert suggestion.origin == nil + end + + test "builtin types - retrieve info from basic types" do + buffer = "defmodule My, do: @type my_type :: int" + + [_, suggestion | _] = suggestions_by_type(:type_spec, buffer) + + assert suggestion.spec == "@type integer()" + assert suggestion.signature == "integer()" + assert suggestion.arity == 0 + assert suggestion.doc == "An integer number" + assert suggestion.origin == nil + end + + test "erlang types" do + buffer = "defmodule My, do: @type my_type :: :erlang.time_" + + suggestions = suggestions_by_type(:type_spec, buffer) + + assert [ + %{ + arity: 0, + doc: summary, + name: "time_unit", + origin: ":erlang", + signature: "time_unit()", + spec: + "@type time_unit() ::\n pos_integer()\n | :second\n | :millisecond\n | :microsecond\n | :nanosecond\n | :native\n | :perf_counter\n | deprecated_time_unit()", + type: :type_spec + } + ] = suggestions + + if System.otp_release() |> String.to_integer() >= 23 do + assert summary =~ "Supported time unit representations:" + end + end + + test "no erlang private types" do + buffer = "defmodule My, do: @type my_type :: :erlang.cpu_topo" + + suggestions = suggestions_by_type(:type_spec, buffer) + + assert [] == suggestions + end + + test "type with @typedoc false" do + buffer = + "defmodule My, do: @type my_type :: ElixirSenseExample.ModuleWithDocs.some_type_doc_false" + + suggestions = suggestions_by_type(:type_spec, buffer) + + assert [ + %{ + arity: 0, + doc: "", + name: "some_type_doc_false", + origin: "ElixirSenseExample.ModuleWithDocs", + signature: "some_type_doc_false()", + spec: "@type some_type_doc_false() ::" <> _, + type: :type_spec, + metadata: %{} + } + ] = suggestions + end + + test "local types from metadata" do + buffer = """ + defmodule MyModule do + @typep my_local_t :: integer + @typep my_local_arg_t(a, b) :: {a, b} + @type my_type :: my_loc + # ^ + end + """ + + list = + Suggestion.suggestions(buffer, 4, 26) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + assert [suggestion1, suggestion2] = list + + assert %{ + arity: 0, + name: "my_local_t", + origin: "MyModule", + type: :type_spec, + signature: "my_local_t()", + args_list: [], + doc: "", + spec: "@typep my_local_t :: integer", + metadata: %{} + } == suggestion2 + + assert %{ + arity: 2, + name: "my_local_arg_t", + origin: "MyModule", + type: :type_spec, + signature: "my_local_arg_t(a, b)", + args_list: ["a", "b"], + doc: "", + spec: "@typep my_local_arg_t(a, b) :: {a, b}", + metadata: %{} + } == suggestion1 + end + + test "suggest local types from metadata even if defined after the cursor" do + buffer = """ + defmodule MyModule do + @type my_type :: my_loc + # ^ + + @typep my_local_t :: integer + end + """ + + list = + Suggestion.suggestions(buffer, 2, 26) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + assert [%{name: "my_local_t"}] = list + end + + test "return docs and meta on local types" do + buffer = """ + defmodule MyModule do + @type my_type :: my_loc + # ^ + + @typedoc "Some" + @typedoc since: "1.2.3" + @type my_local_t :: integer + end + """ + + list = + Suggestion.suggestions(buffer, 2, 26) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + assert [%{name: "my_local_t", doc: "Some", metadata: %{since: "1.2.3"}}] = list + end + + test "local types from metadata external call - private types are not suggested" do + buffer = """ + defmodule MyModule do + @type my_local_t :: integer + @typep my_local_arg_t(a, b) :: {a, b} + @type my_type :: MyModule.my_loc + # ^ + end + """ + + list = + Suggestion.suggestions(buffer, 4, 35) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + assert [suggestion1] = list + + assert %{ + arity: 0, + name: "my_local_t", + origin: "MyModule", + type: :type_spec, + signature: "my_local_t()", + args_list: [], + doc: "", + spec: "@type my_local_t :: integer", + metadata: %{} + } == suggestion1 + end + + test "remote public and opaque types from metadata" do + buffer = """ + defmodule SomeModule do + @typep my_local_priv_t :: integer + @type my_local_pub_t(a, b) :: {a, b} + @opaque my_local_op_t() :: my_local_priv_t + end + + defmodule MyModule do + alias SomeModule, as: Some + @type my_type :: Some.my_loc + # ^ + end + """ + + list = + Suggestion.suggestions(buffer, 9, 31) + |> Enum.filter(fn %{type: t} -> t == :type_spec end) + + assert [suggestion1, suggestion2] = list + + assert %{ + arity: 2, + name: "my_local_pub_t", + origin: "SomeModule", + type: :type_spec, + signature: "my_local_pub_t(a, b)", + args_list: ["a", "b"], + doc: "", + spec: "@type my_local_pub_t(a, b) :: {a, b}", + metadata: %{} + } == suggestion2 + + assert %{ + arity: 0, + name: "my_local_op_t", + origin: "SomeModule", + type: :type_spec, + signature: "my_local_op_t()", + args_list: [], + doc: "", + spec: "@opaque my_local_op_t()", + metadata: %{opaque: true} + } == suggestion1 + end + end + + test "suggestion understands alias shadowing" do + # ordinary alias + buffer = """ + defmodule ElixirSenseExample.OtherModule do + alias ElixirSenseExample.SameModule + def some_fun() do + SameModule.te + end + end + """ + + assert [ + %{origin: "ElixirSenseExample.SameModule"} + ] = Suggestion.suggestions(buffer, 4, 17) + + # alias shadowing scope/inherited aliases + buffer = """ + defmodule ElixirSenseExample.Abc.SameModule do + alias List, as: SameModule + alias ElixirSenseExample.SameModule + def some_fun() do + SameModule.te + end + end + """ + + assert [ + %{origin: "ElixirSenseExample.SameModule"} + ] = Suggestion.suggestions(buffer, 5, 17) + + buffer = """ + defmodule ElixirSenseExample.Abc.SameModule do + require Logger, as: ModuleB + require ElixirSenseExample.SameModule, as: SameModule + SameModule.so + end + """ + + assert [ + %{origin: "ElixirSenseExample.SameModule"} + ] = Suggestion.suggestions(buffer, 4, 15) + end + + test "operator" do + buffer = """ + defmodule ElixirSenseExample.OtherModule do + def some_fun() do + a + + end + end + """ + + assert [%{name: "+"}, %{name: "+"}, %{name: "++"}] = + Suggestion.suggestions(buffer, 3, 8) |> Enum.filter(&("#{&1.name}" =~ "+")) + end + + test "sigil" do + buffer = """ + defmodule ElixirSenseExample.OtherModule do + def some_fun() do + ~ + end + end + """ + + suggestions = Suggestion.suggestions(buffer, 3, 6) + + assert [ + %{ + args: "term, modifiers", + arity: 2, + name: "~w", + summary: "Handles the sigil `~w` for list of words.", + type: :macro + } + ] = suggestions |> Enum.filter(&(&1.name == "~w")) + end + + test "bitstring options" do + buffer = """ + defmodule ElixirSenseExample.OtherModule do + alias ElixirSenseExample.SameModule + def some_fun() do + <> + end + end + """ + + options = + Suggestion.suggestions(buffer, 4, 12) + |> Enum.filter(&(&1.type == :bitstring_option)) + |> Enum.map(& &1.name) + + assert "integer" in options + assert "native" in options + assert "signed" in options + + buffer = """ + defmodule ElixirSenseExample.OtherModule do + alias ElixirSenseExample.SameModule + def some_fun() do + <> + end + end + """ + + ["integer"] = + Suggestion.suggestions(buffer, 4, 15) + |> Enum.filter(&(&1.type == :bitstring_option)) + |> Enum.map(& &1.name) + + buffer = """ + defmodule ElixirSenseExample.OtherModule do + alias ElixirSenseExample.SameModule + def some_fun() do + <> + end + end + """ + + options = + Suggestion.suggestions(buffer, 4, 33) + |> Enum.filter(&(&1.type == :bitstring_option)) + |> Enum.map(& &1.name) + + assert "unit" in options + assert "size" in options + + buffer = """ + defmodule ElixirSenseExample.OtherModule do + alias ElixirSenseExample.SameModule + def some_fun() do + <> + end + end + """ + + ["native"] = + Suggestion.suggestions(buffer, 4, 35) + |> Enum.filter(&(&1.type == :bitstring_option)) + |> Enum.map(& &1.name) + end + + # TODO change that to only output max arity + test "function with default args generate multiple entries" do + buffer = """ + ElixirSenseExample.FunctionsWithTheSameName.all + """ + + assert [ + %{ + arity: 1, + def_arity: 2, + name: "all?", + summary: "all?/2 docs", + type: :function + }, + %{ + arity: 2, + def_arity: 2, + name: "all?", + summary: "all?/2 docs", + type: :function + } + ] = Suggestion.suggestions(buffer, 1, 48) |> Enum.filter(&(&1[:name] == "all?")) + end + + test "functions with the same name but different arities generates independent entries" do + buffer = """ + ElixirSenseExample.FunctionsWithTheSameName.con + """ + + assert [ + %{ + arity: 1, + def_arity: 1, + name: "concat", + summary: "concat/1 docs", + type: :function + }, + %{ + arity: 2, + def_arity: 2, + name: "concat", + summary: "concat/2 docs", + type: :function + } + ] = + Suggestion.suggestions(buffer, 1, 48) |> Enum.filter(&(&1[:name] == "concat")) + end + + test "function with default args from metadata" do + buffer = """ + defmodule SomeSchema do + def my_func(a, b \\\\ "") + def my_func(1, b), do: :ok + def my_func(2, b), do: :ok + + def d() do + my_ + end + end + """ + + suggestions = Suggestion.suggestions(buffer, 7, 8) + + assert [ + %{args: "a, b \\\\ \"\"", arity: 1, def_arity: 2}, + %{args: "a, b \\\\ \"\"", arity: 2, def_arity: 2} + ] = suggestions + end + + test "records from metadata" do + buffer = """ + defmodule SomeSchema do + require Record + Record.defrecord(:user, name: "john", age: 25) + @type user :: record(:user, name: String.t(), age: integer) + + def d() do + w = us + end + end + """ + + suggestions = Suggestion.suggestions(buffer, 7, 11) + + assert [ + %{ + args: "args \\\\ []", + arity: 0, + name: "user", + summary: "", + type: :macro, + args_list: ["args \\\\ []"], + def_arity: 1, + metadata: %{}, + origin: "SomeSchema", + snippet: nil, + spec: "", + visibility: :public + }, + %{ + args: "args \\\\ []", + arity: 1, + name: "user", + summary: "", + type: :macro, + args_list: ["args \\\\ []"], + def_arity: 1, + metadata: %{}, + origin: "SomeSchema", + snippet: nil, + spec: "", + visibility: :public + }, + %{ + args: "record, args", + args_list: ["record", "args"], + arity: 2, + def_arity: 2, + metadata: %{}, + name: "user", + origin: "SomeSchema", + snippet: nil, + spec: "", + summary: "", + type: :macro, + visibility: :public + } + ] = suggestions |> Enum.filter(&(&1.name == "user")) + end + + test "records from introspection" do + buffer = """ + defmodule SomeSchema do + require ElixirSenseExample.ModuleWithRecord, as: M + + def d() do + w = M.us + end + end + """ + + suggestions = Suggestion.suggestions(buffer, 5, 12) + + assert [ + %{ + args: "args \\\\ []", + arity: 0, + name: "user", + summary: "", + type: :macro, + args_list: ["args \\\\ []"], + def_arity: 1, + metadata: %{}, + origin: "ElixirSenseExample.ModuleWithRecord", + snippet: nil, + spec: "", + visibility: :public + }, + %{ + args: "args \\\\ []", + arity: 1, + name: "user", + summary: "", + type: :macro, + args_list: ["args \\\\ []"], + def_arity: 1, + metadata: %{}, + origin: "ElixirSenseExample.ModuleWithRecord", + snippet: nil, + spec: "", + visibility: :public + }, + %{ + args: "record, args", + args_list: ["record", "args"], + arity: 2, + def_arity: 2, + metadata: %{}, + name: "user", + origin: "ElixirSenseExample.ModuleWithRecord", + snippet: nil, + spec: "", + summary: "", + type: :macro, + visibility: :public + } + ] = suggestions |> Enum.filter(&(&1.name == "user")) + end + + defp suggestions_by_type(type, buffer) do + {line, column} = get_last_line_and_column(buffer) + suggestions_by_type(type, buffer, line, column) + end + + defp suggestions_by_type(type, buffer, line, column) do + buffer + |> add_aliases("Local, Remote") + |> Suggestion.suggestions(line + 1, column) + |> Enum.filter(fn %{type: t} -> t == type end) + |> Enum.sort() + end + + defp suggestions_by_name(name, buffer) do + {line, column} = get_last_line_and_column(buffer) + suggestions_by_name(name, buffer, line, column) + end + + defp suggestions_by_name(name, buffer, line, column) do + buffer + |> add_aliases("Local, Remote") + |> Suggestion.suggestions(line + 1, column) + |> Enum.filter(fn + %{name: n} -> n == name + _ -> false + end) + |> Enum.sort() + end + + defp suggestion_by_name(name, buffer) do + {line, column} = get_last_line_and_column(buffer) + suggestion_by_name(name, buffer, line, column) + end + + defp suggestion_by_name(name, buffer, line, column) do + [suggestion] = suggestions_by_name(name, buffer, line, column) + suggestion + end + + defp get_last_line_and_column(buffer) do + str_lines = buffer |> Source.split_lines() + line = length(str_lines) + column = (str_lines |> List.last() |> String.length()) + 1 + {line, column} + end + + defp add_aliases(buffer, aliases) do + "alias ElixirSenseExample.ModuleWithTypespecs.{#{aliases}}\n" <> buffer + end + + def cursors(text) do + {_, cursors} = + ElixirSense.Core.Source.walk_text(text, {false, []}, fn + "#", rest, _, _, {_comment?, cursors} -> + {rest, {true, cursors}} + + "\n", rest, _, _, {_comment?, cursors} -> + {rest, {false, cursors}} + + "^", rest, line, col, {true, cursors} -> + {rest, {true, [%{line: line - 1, col: col} | cursors]}} + + _, rest, _, _, acc -> + {rest, acc} + end) + + Enum.reverse(cursors) + end + + def suggestions(buffer, cursor) do + Suggestion.suggestions(buffer, cursor.line, cursor.col) + end + + def suggestions(buffer, cursor, type) do + suggestions(buffer, cursor) + |> Enum.filter(fn s -> s.type == type end) + end + + def suggestions_by_kind(buffer, cursor, kind) do + suggestions(buffer, cursor) + |> Enum.filter(fn s -> s[:kind] == kind end) + end +end diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index 0d317a657..ad4be0f25 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -473,11 +473,10 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do {:ok, %{"items" => items}} = Completion.completion(parser_context, line, char, @supports) - assert [item] = items + assert [item] = items |> Enum.filter(&(&1["label"] == "ExampleStruct")) # 22 is struct assert item["kind"] == 22 - assert item["label"] == "ExampleStruct" assert item["labelDetails"]["detail"] == "struct" assert item["labelDetails"]["description"] == diff --git a/apps/language_server/test/providers/definition/locator_test.exs b/apps/language_server/test/providers/definition/locator_test.exs new file mode 100644 index 000000000..9ee91a87d --- /dev/null +++ b/apps/language_server/test/providers/definition/locator_test.exs @@ -0,0 +1,1847 @@ +defmodule ElixirLS.LanguageServer.Providers.Definition.LocatorTest do + use ExUnit.Case, async: true + alias ElixirLS.LanguageServer.Providers.Definition.Locator + alias ElixirLS.LanguageServer.Location + alias ElixirSense.Core.Source + + test "don't crash on empty buffer" do + refute Locator.definition("", 1, 1) + end + + test "don't error on __MODULE__ when no module" do + assert nil == Locator.definition("__MODULE__", 1, 1) + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "find module definition inside Phoenix's scope" do + _define_existing_atom = ExampleWeb + + buffer = """ + defmodule ExampleWeb.Router do + import Phoenix.Router + + scope "/", ExampleWeb do + get "/", PageController, :home + end + end + """ + + %Location{type: :module, file: file, line: line, column: column} = + Locator.definition(buffer, 5, 15) + + assert file =~ "language_server/test/support/plugins/phoenix/page_controller.ex" + assert read_line(file, {line, column}) =~ "ExampleWeb.PageController" + end + end + + test "find definition of aliased modules in `use`" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.UseExample + use UseExample + # ^ + end + """ + + %Location{type: :module, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 12) + + assert file =~ "language_server/test/support/use_example.ex" + assert read_line(file, {line, column}) =~ "ElixirSenseExample.UseExample" + end + + @tag requires_source: true + test "find definition of functions from Kernel" do + buffer = """ + defmodule MyModule do + #^ + end + """ + + %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 1, 2) + + assert file =~ "lib/elixir/lib/kernel.ex" + assert read_line(file, {line, column}) =~ "defmodule(" + end + + @tag requires_source: true + test "find definition of functions from Kernel.SpecialForms" do + buffer = """ + defmodule MyModule do + import List + ^ + end + """ + + %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 2, 4) + + assert file =~ "lib/elixir/lib/kernel/special_forms.ex" + assert read_line(file, {line, column}) =~ "import" + end + + test "find definition of functions from imports" do + buffer = """ + defmodule MyModule do + import ElixirSenseExample.ModuleWithFunctions + function_arity_zero() + #^ + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 4) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "function_arity_zero" + end + + test "find definition of functions from aliased modules" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + MyMod.function_arity_one(42) + # ^ + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 11) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "function_arity_one" + end + + test "find definition of macros from required modules" do + buffer = """ + defmodule MyModule do + require ElixirSenseExample.BehaviourWithMacrocallback.Impl, as: Macros + Macros.some(1) + # ^ + end + """ + + %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 13) + + assert file =~ "language_server/test/support/behaviour_with_macrocallbacks.ex" + assert read_line(file, {line, column}) =~ "some" + end + + test "find definition of functions piped from aliased modules" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + 42 |> MyMod.function_arity_one() + # ^ + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 17) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "function_arity_one" + end + + test "find definition of functions captured from aliased modules" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + &MyMod.function_arity_one/1 + # ^ + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 17) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "function_arity_one" + end + + test "find function definition macro generated" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.MacroGenerated, as: Local + Local.my_fun() + # ^ + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 12) + + assert file =~ "language_server/test/support/macro_generated.ex" + assert read_line(file, {line, column}) =~ "ElixirSenseExample.Macros.go" + end + + test "find metadata module" do + buffer = """ + defmodule Some do + def my_func, do: "not this one" + end + + defmodule MyModule do + def main, do: Some.my_func() + # ^ + end + """ + + assert %Location{type: :module, file: nil, line: 1, column: 1} = + Locator.definition(buffer, 6, 19) + end + + @tag capture_log: true + test "find remote module" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: MyMod + def main, do: MyMod.my_func() + # ^ + end + """ + + assert %Location{type: :module, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 19) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + + assert read_line(file, {line, column}) =~ + "defmodule ElixirSenseExample.FunctionsWithDefaultArgs do" + end + + @tag capture_log: true + test "find remote module - fallback to docs" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs1, as: MyMod + def main, do: MyMod.my_func() + # ^ + end + """ + + assert %Location{type: :module, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 19) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "@moduledoc \"example module\"" + end + + test "find definition for the correct arity of function - on fn call" do + buffer = """ + defmodule MyModule do + def main, do: my_func("a", "b") + # ^ + def my_func, do: "not this one" + def my_func(a, b), do: a <> b + end + """ + + assert %Location{type: :function, file: nil, line: 5, column: 3} = + Locator.definition(buffer, 2, 18) + end + + test "find definition for the correct arity of function - on fn call with default arg" do + buffer = """ + defmodule MyModule do + def main, do: my_func("a") + # ^ + def my_func, do: "not this one" + def my_func(a, b \\\\ ""), do: a <> b + end + """ + + assert %Location{type: :function, file: nil, line: 5, column: 3} = + Locator.definition(buffer, 2, 18) + end + + test "find metadata function head for the correct arity of function - on fn call with default arg" do + buffer = """ + defmodule MyModule do + def main, do: {my_func(), my_func("a"), my_func(1, 2, 3)} + # ^ + def my_func, do: "not this one" + def my_func(a, b \\\\ "") + def my_func(1, b), do: "1" <> b + def my_func(2, b), do: "2" <> b + def my_func(1, 2, 3), do: :ok + end + """ + + assert %Location{type: :function, file: nil, line: 5, column: 3} = + Locator.definition(buffer, 2, 30) + end + + @tag capture_log: true + test "find remote function head for the correct arity of function - on fn call with default arg" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: {F.my_func(), F.my_func("a"), F.my_func(1, 2, 3)} + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 34) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "my_func(a, b \\\\ \"\")" + end + + @tag capture_log: true + test "find remote function head for the lowest matching arity of function in incomplete code" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func( + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "def my_func," + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func(1 + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "def my_func(a, b \\\\ \"\")" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func(1, 2, + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "def my_func(1, 2, 3)" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func(1, 2, 3, + end + """ + + # too many arguments + + assert nil == Locator.definition(buffer, 3, 20) + end + + @tag capture_log: true + test "find remote function head for the correct arity of function - on fn call with default arg - fallback to docs" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs1, as: F + def main, do: {F.my_func(), F.my_func("a"), F.my_func(1, 2, 3)} + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 34) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "@doc \"2 params version\"" + end + + @tag capture_log: true + test "find remote function head for the lowest matching arity of function in incomplete code - fallback to docs" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs1, as: F + def main, do: F.my_func( + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "@doc \"no params version\"" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs1, as: F + def main, do: F.my_func(1 + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "@doc \"2 params version\"" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs1, as: F + def main, do: F.my_func(1, 2, + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "@doc \"3 params version\"" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs1, as: F + def main, do: F.my_func(1, 2, 3, + end + """ + + # too many arguments + + assert nil == Locator.definition(buffer, 3, 20) + end + + @tag capture_log: true + test "find remote function head for the correct arity of function - on function capture" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: &F.my_func/1 + end + """ + + assert %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 21) + + assert file =~ "language_server/test/support/functions_with_default_args.ex" + assert read_line(file, {line, column}) =~ "my_func(a, b \\\\ \"\")" + end + + test "find definition for the correct arity of function - on fn call with pipe" do + buffer = """ + defmodule MyModule do + def main, do: "a" |> my_func("b") + # ^ + def my_func, do: "not this one" + def my_func(a, b), do: a <> b + end + """ + + assert %Location{type: :function, file: nil, line: 5, column: 3} = + Locator.definition(buffer, 2, 24) + end + + test "find definition for the correct arity of function - on fn definition" do + buffer = """ + defmodule MyModule do + def my_func, do: "not this one" + def my_func(a, b \\\\ nil) + def my_func(a, b), do: a <> b + end + """ + + assert %Location{type: :function, file: nil, line: 3, column: 3} = + Locator.definition(buffer, 4, 9) + end + + test "find definition for function - on var call" do + buffer = """ + defmodule A do + @callback abc() :: any() + end + + defmodule B do + @behaviour A + + def abc, do: :ok + end + + b = B + b.abc() + """ + + assert %Location{type: :function, file: nil, line: 8, column: 3} = + Locator.definition(buffer, 12, 4) + end + + test "find definition of delegated functions" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + MyMod.delegated_function() + # ^ + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 11) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "delegated_function" + end + + test "find definition of modules" do + buffer = """ + defmodule MyModule do + alias List, as: MyList + ElixirSenseExample.ModuleWithFunctions.function_arity_zero() + # ^ + end + """ + + %Location{type: :module, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 23) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "ElixirSenseExample.ModuleWithFunctions do" + end + + test "find definition of modules in multi alias syntax" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithDocs + alias ElixirSenseExample.{Some, ModuleWithDocs} + end + """ + + %Location{type: :module, file: file_1, line: line_1} = Locator.definition(buffer, 2, 30) + + %Location{type: :module, file: file_2, line: line_2} = Locator.definition(buffer, 3, 38) + + assert file_1 == file_2 + assert line_1 == line_2 + end + + test "find definition of erlang modules" do + buffer = """ + defmodule MyModule do + def dup(x) do + :lists.duplicate(2, x) + # ^ + end + end + """ + + %Location{type: :module, file: file, line: 20, column: 1} = + Locator.definition(buffer, 3, 7) + + assert file =~ "/src/lists.erl" + end + + test "find definition of remote erlang functions" do + buffer = """ + defmodule MyModule do + def dup(x) do + :lists.duplicate(2, x) + # ^ + end + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 15) + + assert file =~ "/src/lists.erl" + assert read_line(file, {line, column}) =~ "duplicate(N, X)" + end + + test "find definition of remote erlang functions from preloaded module" do + buffer = """ + defmodule MyModule do + def dup(x) do + :erlang.start_timer(2, x, 4) + # ^ + end + end + """ + + %Location{type: :function, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 15) + + assert file =~ "/src/erlang.erl" + assert read_line(file, {line, column}) =~ "start_timer(_Time, _Dest, _Msg)" + end + + test "non existing modules" do + buffer = """ + defmodule MyModule do + SilverBulletModule.run + end + """ + + refute Locator.definition(buffer, 2, 24) + end + + test "cannot find map field calls" do + buffer = """ + defmodule MyModule do + env = __ENV__ + IO.puts(env.file) + # ^ + end + """ + + refute Locator.definition(buffer, 3, 16) + end + + test "cannot find map fields" do + buffer = """ + defmodule MyModule do + var = %{count: 1} + # ^ + end + """ + + refute Locator.definition(buffer, 2, 12) + end + + test "preloaded modules" do + buffer = """ + defmodule MyModule do + :erlang.node + # ^ + end + """ + + assert %Location{line: 20, column: 1, type: :module, file: file} = + Locator.definition(buffer, 2, 5) + + assert file =~ "/src/erlang.erl" + end + + test "find built-in functions" do + # module_info is defined by default for every elixir and erlang module + # __info__ is defined for every elixir module + # behaviour_info is defined for every behaviour and every protocol + buffer = """ + defmodule MyModule do + ElixirSenseExample.ModuleWithFunctions.module_info() + # ^ + ElixirSenseExample.ModuleWithFunctions.__info__(:macros) + # ^ + ElixirSenseExample.ExampleBehaviour.behaviour_info(:callbacks) + # ^ + end + """ + + assert %{column: column, file: file, line: line, type: :function} = + Locator.definition(buffer, 2, 42) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "ElixirSenseExample.ModuleWithFunctions do" + + assert %Location{type: :function} = Locator.definition(buffer, 4, 42) + + assert %Location{type: :function} = Locator.definition(buffer, 6, 42) + end + + test "built-in functions cannot be called locally" do + # module_info is defined by default for every elixir and erlang module + # __info__ is defined for every elixir module + # behaviour_info is defined for every behaviour and every protocol + buffer = """ + defmodule MyModule do + import GenServer + @ callback cb() :: term + module_info() + #^ + __info__(:macros) + #^ + behaviour_info(:callbacks) + #^ + end + """ + + refute Locator.definition(buffer, 4, 5) + + refute Locator.definition(buffer, 6, 5) + + refute Locator.definition(buffer, 8, 5) + end + + test "does not find built-in erlang functions" do + buffer = """ + defmodule MyModule do + :erlang.orelse() + # ^ + :erlang.or() + # ^ + end + """ + + refute Locator.definition(buffer, 2, 14) + + refute Locator.definition(buffer, 4, 12) + end + + test "find definition of variables" do + buffer = """ + defmodule MyModule do + def func do + var1 = 1 + var2 = 2 + var1 = 3 + IO.puts(var1 + var2) + end + end + """ + + assert Locator.definition(buffer, 6, 13) == %Location{ + type: :variable, + file: nil, + line: 5, + column: 5 + } + + assert Locator.definition(buffer, 6, 21) == %Location{ + type: :variable, + file: nil, + line: 4, + column: 5 + } + end + + test "find definition of variables defined on the next line" do + buffer = """ + defmodule MyModule do + def func do + var1 = + 1 + end + end + """ + + assert Locator.definition(buffer, 3, 5) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 5 + } + end + + test "find definition of functions when name not same as variable" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + + def a do + my_fun1 = 1 + my_fun() + end + end + """ + + assert Locator.definition(buffer, 6, 6) == %Location{ + type: :function, + file: nil, + line: 2, + column: 3 + } + end + + test "find definition of functions when name same as variable - parens preferes function" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + + def a do + my_fun = 1 + my_fun() + end + end + """ + + assert Locator.definition(buffer, 6, 6) == %Location{ + type: :function, + file: nil, + line: 2, + column: 3 + } + end + + test "find definition of variables when name same as function - no parens preferes variable" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + + def a do + my_fun = 1 + my_fun + end + end + """ + + assert Locator.definition(buffer, 6, 6) == %Location{ + type: :variable, + file: nil, + line: 5, + column: 5 + } + end + + test "find definition of variables when name same as function" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :error + + def a do + my_fun = fn -> :ok end + my_fun.() + end + end + """ + + assert Locator.definition(buffer, 6, 6) == %Location{ + type: :variable, + file: nil, + line: 5, + column: 5 + } + end + + test "find definition for a redefined variable" do + buffer = """ + defmodule MyModule do + def my_fun(var) do + var = 1 + var + + var + end + end + """ + + # `var` defined in the function header + assert Locator.definition(buffer, 3, 15) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 14 + } + + # `var` redefined in the function body + assert Locator.definition(buffer, 5, 5) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 5 + } + end + + test "find definition of a variable in a guard" do + buffer = """ + defmodule MyModule do + def my_fun(var) when is_atom(var) do + case var do + var when var > 0 -> var + end + + Enum.map([1, 2], fn x when x > 0 -> x end) + end + end + """ + + assert Locator.definition(buffer, 2, 32) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 14 + } + + assert Locator.definition(buffer, 4, 16) == %Location{ + type: :variable, + file: nil, + line: 4, + column: 7 + } + + assert Locator.definition(buffer, 7, 32) == %Location{ + type: :variable, + file: nil, + line: 7, + column: 25 + } + end + + test "find definition of variables when variable is a function parameter" do + buffer = """ + defmodule MyModule do + def my_fun([h | t]) do + sum = h + my_fun(t) + + if h > sum do + h + sum + else + h = my_fun(t) + sum + h + end + end + end + """ + + # `h` from the function header + assert Locator.definition(buffer, 3, 11) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 15 + } + + assert Locator.definition(buffer, 6, 7) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 15 + } + + # `h` from the if-else scope + assert Locator.definition(buffer, 9, 7) == %Location{ + type: :variable, + file: nil, + line: 8, + column: 7 + } + + # `t` + assert Locator.definition(buffer, 8, 18) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 19 + } + + # `sum` + assert Locator.definition(buffer, 8, 23) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 5 + } + end + + test "find definition of variables from the scope of an anonymous function" do + buffer = """ + defmodule MyModule do + def my_fun(x, y) do + x = Enum.map(x, fn x -> x + y end) + end + end + """ + + # `x` from the `my_fun` function header + assert Locator.definition(buffer, 3, 18) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 14 + } + + # `y` from the `my_fun` function header + assert Locator.definition(buffer, 3, 33) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 17 + } + + # `x` from the anonymous function + assert Locator.definition(buffer, 3, 29) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 24 + } + + # redefined `x` + assert Locator.definition(buffer, 3, 5) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 5 + } + end + + test "find definition of variables inside multiline struct" do + buffer = """ + defmodule MyModule do + def go do + %Some{ + filed: var + } = abc() + end + end + """ + + assert Locator.definition(buffer, 4, 15) == %Location{ + type: :variable, + file: nil, + line: 4, + column: 14 + } + end + + test "find definition of a variable when using pin operator" do + buffer = """ + defmodule MyModule do + def my_fun(a, b) do + case a do + ^b -> b + %{b: ^b} = a -> b + end + end + end + """ + + # `b` + assert Locator.definition(buffer, 4, 8) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 17 + } + + assert Locator.definition(buffer, 4, 13) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 17 + } + + assert Locator.definition(buffer, 5, 13) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 17 + } + + assert Locator.definition(buffer, 5, 23) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 17 + } + + # `a` redefined in a case clause + assert Locator.definition(buffer, 5, 18) == %Location{ + type: :variable, + file: nil, + line: 5, + column: 18 + } + end + + test "find definition of attributes" do + buffer = """ + defmodule MyModule do + def func do + @var1 1 + @var2 2 + @var1 3 + IO.puts(@var1 + @var2) + end + end + """ + + assert Locator.definition(buffer, 6, 15) == %Location{ + type: :attribute, + file: nil, + line: 3, + column: 5 + } + + assert Locator.definition(buffer, 6, 24) == %Location{ + type: :attribute, + file: nil, + line: 4, + column: 5 + } + end + + test "find definition of local functions with default args" do + buffer = """ + defmodule MyModule do + def my_fun(a \\\\ 0, b \\\\ nil), do: :ok + + def a do + my_fun() + end + end + """ + + assert Locator.definition(buffer, 5, 6) == %Location{ + type: :function, + file: nil, + line: 2, + column: 3 + } + end + + test "find definition of local __MODULE__" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + + def a do + my_fun1 = 1 + __MODULE__.my_fun() + end + end + """ + + assert Locator.definition(buffer, 6, 6) == %Location{ + type: :module, + file: nil, + line: 1, + column: 1 + } + end + + test "find definition of local functions with __MODULE__" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + + def a do + my_fun1 = 1 + __MODULE__.my_fun() + end + end + """ + + assert Locator.definition(buffer, 6, 17) == %Location{ + type: :function, + file: nil, + line: 2, + column: 3 + } + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "find definition of local functions with __MODULE__ submodule" do + buffer = """ + defmodule MyModule do + defmodule Sub do + def my_fun(), do: :ok + end + + def a do + my_fun1 = 1 + __MODULE__.Sub.my_fun() + end + end + """ + + assert Locator.definition(buffer, 8, 22) == %Location{ + type: :function, + file: nil, + line: 3, + column: 5 + } + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "find definition of local __MODULE__ submodule" do + buffer = """ + defmodule MyModule do + defmodule Sub do + def my_fun(), do: :ok + end + + def a do + my_fun1 = 1 + __MODULE__.Sub.my_fun() + end + end + """ + + assert Locator.definition(buffer, 8, 17) == %Location{ + type: :module, + file: nil, + line: 2, + column: 3 + } + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "find definition of local functions with @attr" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + @attr MyModule + def a do + my_fun1 = 1 + @attr.my_fun() + end + end + """ + + assert Locator.definition(buffer, 6, 13) == %Location{ + type: :function, + file: nil, + line: 2, + column: 3 + } + end + end + + test "find definition of local functions with current module" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + + def a do + my_fun1 = 1 + MyModule.my_fun() + end + end + """ + + assert Locator.definition(buffer, 6, 14) == %Location{ + type: :function, + file: nil, + line: 2, + column: 3 + } + end + + test "find definition of local macro" do + buffer = """ + defmodule MyModule do + defmacrop some(var), do: Macro.expand(var, __CALLER__) + + defmacro other do + some(1) + end + end + """ + + assert Locator.definition(buffer, 5, 6) == %Location{ + type: :macro, + file: nil, + line: 2, + column: 3 + } + end + + test "find definition of local macro on definition" do + buffer = """ + defmodule MyModule do + defmacrop some(var), do: Macro.expand(var, __CALLER__) + + defmacro other do + some(1) + end + end + """ + + assert Locator.definition(buffer, 2, 14) == %Location{ + type: :macro, + file: nil, + line: 2, + column: 3 + } + end + + test "does not find definition of local macro if it's defined after the cursor" do + buffer = """ + defmodule MyModule do + defmacro other do + some(1) + end + + defmacrop some(var), do: Macro.expand(var, __CALLER__) + end + """ + + assert Locator.definition(buffer, 3, 6) == nil + end + + test "find definition of local function even if it's defined after the cursor" do + buffer = """ + defmodule MyModule do + def other do + some(1) + end + + defp some(var), do: :ok + end + """ + + assert Locator.definition(buffer, 3, 6) == %Location{ + type: :function, + file: nil, + line: 6, + column: 3 + } + end + + test "find definition of local functions with alias" do + buffer = """ + defmodule MyModule do + alias MyModule, as: M + def my_fun(), do: :ok + def my_fun(a), do: :ok + + def a do + my_fun1 = 1 + M.my_fun() + end + end + """ + + assert Locator.definition(buffer, 8, 7) == %Location{ + type: :function, + file: nil, + line: 3, + column: 3 + } + end + + test "do not find private function definition" do + buffer = """ + defmodule MyModule do + defmodule Submodule do + defp my_fun(), do: :ok + end + + def a do + MyModule.Submodule.my_fun() + end + end + """ + + refute Locator.definition(buffer, 7, 25) + end + + test "find definition of local module" do + buffer = """ + defmodule MyModule do + defmodule Submodule do + def my_fun(), do: :ok + end + + def a do + MyModule.Submodule.my_fun() + end + end + """ + + assert Locator.definition(buffer, 7, 16) == %Location{ + type: :module, + file: nil, + line: 2, + column: 3 + } + end + + test "find definition of params" do + buffer = """ + defmodule MyModule do + def func(%{a: [var2|_]}) do + var1 = 3 + IO.puts(var1 + var2) + # ^ + end + end + """ + + assert Locator.definition(buffer, 4, 21) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 18 + } + end + + test "find remote type definition" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithTypespecs.Remote + @type a :: Remote.remote_t + # ^ + end + """ + + %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 24) + + assert file =~ "language_server/test/support/module_with_typespecs.ex" + assert read_line(file, {line, column}) =~ ~r/^@type remote_t/ + end + + test "find type definition without @typedoc" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithTypespecs.Remote + @type a :: Remote.remote_option_t + # ^ + end + """ + + %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 24) + + assert file =~ "language_server/test/support/module_with_typespecs.ex" + assert read_line(file, {line, column}) =~ ~r/@type remote_option_t ::/ + end + + test "find opaque type definition" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithTypespecs.Local + @type a :: Local.opaque_t + # ^ + end + """ + + %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 23) + + assert file =~ "language_server/test/support/module_with_typespecs.ex" + assert read_line(file, {line, column}) =~ ~r/@opaque opaque_t/ + end + + test "find type definition macro generated" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.MacroGenerated, as: Local + @type a :: Local.my_type + # ^ + end + """ + + %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 23) + + assert file =~ "language_server/test/support/macro_generated.ex" + assert read_line(file, {line, column}) =~ "ElixirSenseExample.Macros.go" + end + + test "find erlang type definition" do + buffer = """ + defmodule MyModule do + @type a :: :ets.tab + # ^ + end + """ + + %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 2, 20) + + assert file =~ "/src/ets.erl" + assert read_line(file, {line, column}) =~ "-type tab()" + end + + test "find erlang type definition from preloaded module" do + buffer = """ + defmodule MyModule do + @type a :: :erlang.time_unit + # ^ + end + """ + + %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 2, 23) + + assert file =~ "/src/erlang.erl" + assert read_line(file, {line, column}) =~ "-type time_unit()" + end + + test "do not find erlang private type" do + buffer = """ + defmodule MyModule do + @type a :: :erlang.memory_type + # ^ + end + """ + + refute Locator.definition(buffer, 2, 23) + end + + test "builtin types cannot be found" do + buffer = """ + defmodule MyModule do + @type my_type :: integer + # ^ + end + """ + + refute Locator.definition(buffer, 2, 23) + end + + test "builtin elixir types cannot be found" do + buffer = """ + defmodule MyModule do + @type my_type :: Elixir.keyword + # ^ + end + """ + + refute Locator.definition(buffer, 2, 29) + end + + test "find local metadata type definition" do + buffer = """ + defmodule MyModule do + @typep my_t :: integer + + @type remote_list_t :: [my_t] + # ^ + end + """ + + %Location{type: :typespec, file: nil, line: 2, column: 3} = + Locator.definition(buffer, 4, 29) + end + + test "find local metadata type definition even if it's defined after cursor" do + buffer = """ + defmodule MyModule do + @type remote_list_t :: [my_t] + # ^ + + @typep my_t :: integer + end + """ + + %Location{type: :typespec, file: nil, line: 5, column: 3} = + Locator.definition(buffer, 2, 29) + end + + test "find remote metadata type definition" do + buffer = """ + defmodule MyModule.Other do + @type my_t :: integer + @type my_t(a) :: {a, integer} + end + + defmodule MyModule do + alias MyModule.Other + + @type remote_list_t :: [Other.my_t] + # ^ + end + """ + + %Location{type: :typespec, file: nil, line: 2, column: 3} = + Locator.definition(buffer, 9, 35) + end + + test "do not find remote private type definition" do + buffer = """ + defmodule MyModule.Other do + @typep my_t :: integer + @typep my_t(a) :: {a, integer} + end + + defmodule MyModule do + alias MyModule.Other + + @type remote_list_t :: [Other.my_t] + # ^ + end + """ + + refute Locator.definition(buffer, 9, 35) + end + + test "find metadata type for the correct arity" do + buffer = """ + defmodule MyModule do + @type my_type :: integer + @type my_type(a) :: {integer, a} + @type my_type(a, b) :: {integer, a, b} + @type some :: {my_type, my_type(boolean), my_type(integer, integer)} + end + """ + + assert %Location{type: :typespec, file: nil, line: 3, column: 3} = + Locator.definition(buffer, 5, 28) + end + + test "find metadata type for the correct arity - on type definition" do + buffer = """ + defmodule MyModule do + @type my_type :: integer + @type my_type(a) :: {integer, a} + @type my_type(a, b) :: {integer, a, b} + end + """ + + assert %Location{type: :typespec, file: nil, line: 3, column: 3} = + Locator.definition(buffer, 3, 10) + end + + test "find remote type for the correct arity" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: {T.my_type, T.my_type(boolean), T.my_type(1, 2)} + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 32) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "my_type(a)" + end + + test "find remote type for lowest matching arity in incomplete code" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type( + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "@type my_type :: integer" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type(integer + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "@type my_type(a) :: {integer, a}" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type(integer, + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "@type my_type(a, b) :: {integer, a, b}" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type(integer, integer, + end + """ + + # too many arguments + + assert nil == Locator.definition(buffer, 3, 20) + end + + @tag capture_log: true + test "find remote type for the correct arity - fallback to docs" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity1, as: T + @type some :: {T.my_type, T.my_type(boolean), T.my_type(1, 2)} + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 32) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "@typedoc \"one param version\"" + end + + @tag capture_log: true + test "find remote type for lowest matching arity in incomplete code - fallback to docs" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity1, as: T + @type some :: T.my_type( + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "@typedoc \"no params version\"" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity1, as: T + @type some :: T.my_type(integer + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "@typedoc \"one param version\"" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity1, as: T + @type some :: T.my_type(integer, + end + """ + + assert %Location{type: :typespec, file: file, line: line, column: column} = + Locator.definition(buffer, 3, 20) + + assert file =~ "language_server/test/support/types_with_multiple_arity.ex" + assert read_line(file, {line, column}) =~ "@typedoc \"two params version\"" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity1, as: T + @type some :: T.my_type(integer, integer, + end + """ + + # too many arguments + + assert nil == Locator.definition(buffer, 3, 20) + end + + test "find super inside overridable function" do + buffer = """ + defmodule MyModule do + use ElixirSenseExample.OverridableFunctions + + def test(x, y) do + super(x, y) + end + + defmacro required(v) do + super(v) + end + end + """ + + assert %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 5, 6) + + assert file =~ "language_server/test/support/overridable_function.ex" + assert read_line(file, {line, column}) =~ "__using__(_opts)" + + assert %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 9, 6) + + assert file =~ "language_server/test/support/overridable_function.ex" + assert read_line(file, {line, column}) =~ "__using__(_opts)" + end + + test "find super inside overridable callback" do + buffer = """ + defmodule MyModule do + use ElixirSenseExample.OverridableImplementation + + def foo do + super() + end + + defmacro bar(any) do + super(any) + end + end + """ + + assert %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 5, 6) + + assert file =~ "language_server/test/support/overridable_function.ex" + assert read_line(file, {line, column}) =~ "__using__(_opts)" + + assert %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 9, 6) + + assert file =~ "language_server/test/support/overridable_function.ex" + assert read_line(file, {line, column}) =~ "__using__(_opts)" + end + + test "find super inside overridable callback when module is compiled" do + buffer = """ + defmodule ElixirSenseExample.OverridableImplementation.Overrider do + use ElixirSenseExample.OverridableImplementation + + def foo do + super() + end + + defmacro bar(any) do + super(any) + end + end + """ + + assert %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 5, 6) + + assert file =~ "language_server/test/support/overridable_function.ex" + assert read_line(file, {line, column}) =~ "__using__(_opts)" + + assert %Location{type: :macro, file: file, line: line, column: column} = + Locator.definition(buffer, 9, 6) + + assert file =~ "language_server/test/support/overridable_function.ex" + assert read_line(file, {line, column}) =~ "__using__(_opts)" + end + + test "find local type in typespec local def elsewhere" do + buffer = """ + defmodule ElixirSenseExample.Some do + @type some_local :: integer + + def some_local(), do: :ok + + @type user :: {some_local, integer} + + def foo do + some_local + end + end + """ + + assert %Location{type: :typespec, file: nil, line: 2} = Locator.definition(buffer, 6, 20) + + assert %Location{type: :function, file: nil, line: 4} = Locator.definition(buffer, 9, 9) + end + + test "find variable with the same name as special form" do + buffer = """ + defmodule ElixirSenseExample.Some do + def foo do + quote = 123 + abc(quote) + end + end + """ + + assert %Location{type: :variable, file: nil, line: 3} = Locator.definition(buffer, 4, 10) + end + + defp read_line(file, {line, column}) do + file + |> File.read!() + |> Source.split_lines() + |> Enum.at(line - 1) + |> String.slice((column - 1)..-1//1) + end +end diff --git a/apps/language_server/test/providers/execute_command/expand_macro_test.exs b/apps/language_server/test/providers/execute_command/expand_macro_test.exs index 821af4d05..44f5013eb 100644 --- a/apps/language_server/test/providers/execute_command/expand_macro_test.exs +++ b/apps/language_server/test/providers/execute_command/expand_macro_test.exs @@ -132,4 +132,133 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ExpandMacroTest do } end end + + describe "expand full" do + test "without errors" do + buffer = """ + defmodule MyModule do + + end + """ + + code = "use Application" + result = ExpandMacro.expand_full(buffer, code, 2) + + if Version.match?(System.version(), ">= 1.13.0") do + assert result.expand_once =~ + """ + ( + require Application + Application.__using__([]) + ) + """ + |> String.trim() + + assert result.expand =~ + """ + ( + require Application + Application.__using__([]) + ) + """ + |> String.trim() + + assert result.expand_partial =~ + """ + ( + require Application + + ( + @behaviour Application + @doc false + def stop(_state) do + :ok + end + + defoverridable Application + ) + ) + """ + |> String.trim() + + assert result.expand_all =~ + (if Version.match?(System.version(), ">= 1.14.0") do + """ + ( + require Application + + ( + Module.__put_attribute__(MyModule, :behaviour, Application, nil, []) + Module.__put_attribute__(MyModule, :doc, {0, false}, nil, []) + + def stop(_state) do + :ok + end + + Module.make_overridable(MyModule, Application) + """ + else + """ + ( + require Application + + ( + Module.__put_attribute__(MyModule, :behaviour, Application, nil) + Module.__put_attribute__(MyModule, :doc, {0, false}, nil) + + def stop(_state) do + :ok + end + + Module.make_overridable(MyModule, Application) + """ + end) + |> String.trim() + else + assert result.expand_once =~ + """ + ( + require(Application) + Application.__using__([]) + ) + """ + |> String.trim() + end + end + + test "with errors" do + buffer = """ + defmodule MyModule do + + end + """ + + code = "{" + result = ExpandMacro.expand_full(buffer, code, 2) + + assert result.expand_once =~ + """ + "missing terminator: }\ + """ + |> String.trim() + + assert result.expand =~ + """ + "missing terminator: }\ + """ + |> String.trim() + + assert result.expand_partial =~ + """ + "missing terminator: }\ + """ + |> String.trim() + + assert result.expand_all =~ + """ + "missing terminator: }\ + """ + |> String.trim() + end + end end diff --git a/apps/language_server/test/providers/hover/docs_test.exs b/apps/language_server/test/providers/hover/docs_test.exs new file mode 100644 index 000000000..d61b73cc3 --- /dev/null +++ b/apps/language_server/test/providers/hover/docs_test.exs @@ -0,0 +1,2067 @@ +defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do + use ExUnit.Case, async: true + alias ElixirLS.LanguageServer.Providers.Hover.Docs + + test "when no docs do not return Built-in type" do + buffer = """ + hkjnjknjk + """ + + refute Docs.docs(buffer, 1, 2) + end + + test "when empty buffer" do + assert nil == Docs.docs("", 1, 1) + end + + describe "module docs" do + test "module with @moduledoc false" do + %{ + docs: [doc] + } = Docs.docs("ElixirSenseExample.ModuleWithDocFalse", 1, 22) + + assert doc == %{ + module: ElixirSenseExample.ModuleWithDocFalse, + metadata: %{hidden: true, app: :language_server}, + docs: "", + kind: :module + } + end + + test "module with no @moduledoc" do + %{ + docs: [doc] + } = Docs.docs("ElixirSenseExample.ModuleWithNoDocs", 1, 22) + + assert doc == %{ + module: ElixirSenseExample.ModuleWithNoDocs, + metadata: %{app: :language_server}, + docs: "", + kind: :module + } + end + + test "retrieve documentation from modules" do + buffer = """ + defmodule MyModule do + use GenServer + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 2, 8) + + assert doc.module == GenServer + assert doc.metadata == %{app: :elixir} + assert doc.kind == :module + + assert doc.docs =~ """ + A behaviour module for implementing the server of a client-server relation.\ + """ + end + + test "retrieve documentation from metadata modules" do + buffer = """ + defmodule MyLocalModule do + @moduledoc "Some example doc" + @moduledoc since: "1.2.3" + + @callback some() :: :ok + end + + defmodule MyModule do + @behaviour MyLocalModule + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 9, 15) + + assert doc == %{ + module: MyLocalModule, + metadata: %{since: "1.2.3"}, + docs: "Some example doc", + kind: :module + } + end + + test "retrieve documentation from metadata modules on __MODULE__" do + buffer = """ + defmodule MyLocalModule do + @moduledoc "Some example doc" + @moduledoc since: "1.2.3" + + def self() do + __MODULE__ + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 6, 6) + + assert doc == %{ + module: MyLocalModule, + metadata: %{since: "1.2.3"}, + docs: "Some example doc", + kind: :module + } + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "retrieve documentation from metadata modules on __MODULE__ submodule" do + buffer = """ + defmodule MyLocalModule do + defmodule Sub do + @moduledoc "Some example doc" + @moduledoc since: "1.2.3" + end + + def self() do + __MODULE__.Sub + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 8, 17) + + assert doc == %{ + module: MyLocalModule.Sub, + metadata: %{since: "1.2.3"}, + docs: "Some example doc", + kind: :module + } + end + end + + test "retrieve documentation from metadata modules with @moduledoc false" do + buffer = """ + defmodule MyLocalModule do + @moduledoc false + + @callback some() :: :ok + end + + defmodule MyModule do + @behaviour MyLocalModule + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 8, 15) + + assert doc == %{module: MyLocalModule, metadata: %{hidden: true}, docs: "", kind: :module} + end + + test "retrieve documentation from erlang modules" do + buffer = """ + defmodule MyModule do + alias :erlang, as: Erl + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 2, 13) + + assert doc.module == :erlang + assert doc.kind == :module + + if System.otp_release() |> String.to_integer() >= 23 do + assert doc.docs =~ """ + By convention,\ + """ + + assert %{name: "erlang", otp_doc_vsn: {1, 0, 0}} = doc.metadata + end + end + + test "retrieve documentation from modules in 1.2 alias syntax" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithDocs + alias ElixirSenseExample.{Some, ModuleWithDocs} + end + """ + + %{ + docs: docs_1 + } = Docs.docs(buffer, 2, 30) + + %{ + docs: docs_2 + } = Docs.docs(buffer, 2, 38) + + assert docs_1 == docs_2 + end + + test "not existing module docs" do + buffer = """ + defmodule MyModule do + raise NotExistingError, "Error" + end + """ + + refute Docs.docs(buffer, 2, 11) + end + end + + describe "functions and macros" do + test "retrieve documentation from Kernel macro" do + buffer = """ + defmodule MyModule do + + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 1, 2) + + assert %{ + args: ["alias", "do_block"], + function: :defmodule, + module: Kernel, + metadata: %{}, + specs: [], + kind: :macro + } = doc + + assert doc.module == Kernel + assert doc.function == :defmodule + + assert doc.docs =~ """ + Defines a module given by name with the given contents. + """ + end + + test "retrieve documentation from Kernel.SpecialForm macro" do + buffer = """ + defmodule MyModule do + import List + ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 2, 4) + + assert %{ + args: ["module", "opts"], + function: :import, + module: Kernel.SpecialForms, + metadata: %{}, + specs: [], + kind: :macro + } = doc + + assert doc.docs =~ """ + Imports functions and macros\ + """ + end + + test "function with @doc false" do + %{ + docs: [doc] + } = Docs.docs("ElixirSenseExample.ModuleWithDocs.some_fun_doc_false(1)", 1, 40) + + assert doc == %{ + module: ElixirSenseExample.ModuleWithDocs, + metadata: %{hidden: true, defaults: 1, app: :language_server}, + docs: "", + kind: :function, + args: ["a", "b \\\\ nil"], + arity: 2, + function: :some_fun_doc_false, + specs: [] + } + end + + test "function no @doc" do + %{ + docs: [doc] + } = Docs.docs("ElixirSenseExample.ModuleWithDocs.some_fun_no_doc(1)", 1, 40) + + assert doc == %{ + docs: "", + kind: :function, + metadata: %{defaults: 1, app: :language_server}, + module: ElixirSenseExample.ModuleWithDocs, + args: ["a", "b \\\\ nil"], + arity: 2, + function: :some_fun_no_doc, + specs: [] + } + end + + test "retrieve function documentation" do + buffer = """ + defmodule MyModule do + def func(list) do + List.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 12) + + assert %{ + args: ["list"], + function: :flatten, + module: List, + metadata: %{}, + specs: ["@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]"], + kind: :function + } = doc + + assert doc.docs =~ """ + Flattens the given `list` of nested lists. + """ + end + + test "retrieve metadata function documentation" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 12, 20) + + assert doc == %{ + args: ["list"], + function: :flatten, + arity: 1, + module: MyLocalModule, + metadata: %{since: "1.2.3"}, + specs: ["@spec flatten(list()) :: list()"], + docs: "Sample doc", + kind: :function + } + end + + test "retrieve local private metadata function documentation" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + defp flatten(list) do + [] + end + + def func(list) do + flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 10, 7) + + assert doc == %{ + args: ["list"], + arity: 1, + function: :flatten, + module: MyLocalModule, + metadata: %{since: "1.2.3"}, + specs: ["@spec flatten(list()) :: list()"], + docs: "", + kind: :function + } + end + + test "retrieve metadata function documentation - fallback to callback in metadata" do + buffer = """ + defmodule MyBehaviour do + @doc "Sample doc" + @doc since: "1.2.3" + @callback flatten(list()) :: list() + end + + defmodule MyLocalModule do + @behaviour MyBehaviour + + @impl true + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 18, 20) + + assert doc == %{ + args: ["list"], + arity: 1, + function: :flatten, + kind: :function, + metadata: %{implementing: MyBehaviour, since: "1.2.3", hidden: true}, + module: MyLocalModule, + specs: ["@callback flatten(list()) :: list()"], + docs: "Sample doc" + } + end + + test "retrieve metadata function documentation - fallback to callback in metadata no @impl" do + buffer = """ + defmodule MyBehaviour do + @doc "Sample doc" + @doc since: "1.2.3" + @callback flatten(list()) :: list() + end + + defmodule MyLocalModule do + @behaviour MyBehaviour + + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 17, 20) + + assert doc == %{ + args: ["list"], + arity: 1, + function: :flatten, + kind: :function, + metadata: %{implementing: MyBehaviour, since: "1.2.3"}, + module: MyLocalModule, + specs: ["@callback flatten(list()) :: list()"], + docs: "Sample doc" + } + end + + test "retrieve metadata function documentation - fallback to protocol function in metadata" do + buffer = """ + defprotocol BB do + @doc "asdf" + @doc since: "1.2.3" + @spec go(t) :: integer() + def go(t) + end + + defimpl BB, for: String do + def go(t), do: "" + end + + defmodule MyModule do + def func(list) do + BB.String.go(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 14, 16) + + assert doc == %{ + args: ["t"], + function: :go, + arity: 1, + kind: :function, + metadata: %{implementing: BB, since: "1.2.3"}, + module: BB.String, + specs: ["@callback go(t) :: integer()"], + docs: "asdf" + } + end + + test "retrieve documentation of local macro" do + buffer = """ + defmodule MyModule do + defmacrop some(var), do: Macro.expand(var, __CALLER__) + + defmacro other do + some(1) + end + end + """ + + assert %{ + docs: [_doc] + } = Docs.docs(buffer, 5, 6) + end + + test "find definition of local macro on definition" do + buffer = """ + defmodule MyModule do + defmacrop some(var), do: Macro.expand(var, __CALLER__) + + defmacro other do + some(1) + end + end + """ + + assert %{ + docs: [_doc] + } = Docs.docs(buffer, 2, 14) + end + + test "does not find definition of local macro if it's defined after the cursor" do + buffer = """ + defmodule MyModule do + defmacro other do + some(1) + end + + defmacrop some(var), do: Macro.expand(var, __CALLER__) + end + """ + + assert Docs.docs(buffer, 3, 6) == nil + end + + test "find definition of local function even if it's defined after the cursor" do + buffer = """ + defmodule MyModule do + def other do + some(1) + end + + defp some(var), do: :ok + end + """ + + assert %{ + docs: [_doc] + } = Docs.docs(buffer, 3, 6) + end + + test "retrieve metadata macro documentation - fallback to macrocallback in metadata" do + buffer = """ + defmodule MyBehaviour do + @doc "Sample doc" + @doc since: "1.2.3" + @macrocallback flatten(list()) :: list() + end + + defmodule MyLocalModule do + @behaviour MyBehaviour + + @impl true + defmacro flatten(list) do + [] + end + end + + defmodule MyModule do + require MyLocalModule + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 19, 20) + + assert doc == %{ + args: ["list"], + arity: 1, + function: :flatten, + kind: :macro, + metadata: %{implementing: MyBehaviour, since: "1.2.3", hidden: true}, + module: MyLocalModule, + specs: ["@macrocallback flatten(list()) :: list()"], + docs: "Sample doc" + } + end + + test "retrieve metadata function documentation - fallback to callback" do + buffer = """ + defmodule MyLocalModule do + @behaviour ElixirSenseExample.BehaviourWithMeta + + @impl true + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 12, 20) + + assert doc == %{ + args: ["list"], + function: :flatten, + arity: 1, + kind: :function, + metadata: %{ + implementing: ElixirSenseExample.BehaviourWithMeta, + implementing_module_app: :language_server, + since: "1.2.3", + hidden: true + }, + module: MyLocalModule, + specs: ["@callback flatten(list()) :: list()"], + docs: "Sample doc" + } + end + + test "retrieve metadata function documentation - fallback to callback no @impl" do + buffer = """ + defmodule MyLocalModule do + @behaviour ElixirSenseExample.BehaviourWithMeta + + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 11, 20) + + assert doc == %{ + args: ["list"], + function: :flatten, + arity: 1, + kind: :function, + metadata: %{ + implementing: ElixirSenseExample.BehaviourWithMeta, + implementing_module_app: :language_server, + since: "1.2.3" + }, + module: MyLocalModule, + specs: ["@callback flatten(list()) :: list()"], + docs: "Sample doc" + } + end + + test "retrieve metadata function documentation - fallback to erlang callback" do + buffer = """ + defmodule MyLocalModule do + @behaviour :gen_statem + + @impl true + def init(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.init(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 12, 20) + + assert %{ + args: ["list"], + function: :init, + module: MyLocalModule, + kind: :function + } = doc + + if System.otp_release() |> String.to_integer() >= 23 do + assert doc.docs =~ + "this function is called by" + + assert %{since: "OTP 19.0", implementing: :gen_statem} = doc.metadata + end + end + + test "retrieve metadata macro documentation - fallback to macrocallback" do + buffer = """ + defmodule MyLocalModule do + @behaviour ElixirSenseExample.BehaviourWithMeta + + @impl true + defmacro bar(list) do + [] + end + end + + defmodule MyModule do + require MyLocalModule + def func(list) do + MyLocalModule.bar(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 13, 20) + + assert doc == %{ + args: ["list"], + arity: 1, + function: :bar, + module: MyLocalModule, + metadata: %{ + since: "1.2.3", + implementing: ElixirSenseExample.BehaviourWithMeta, + implementing_module_app: :language_server, + hidden: true + }, + specs: ["@macrocallback bar(integer()) :: Macro.t()"], + docs: "Docs for bar", + kind: :macro + } + end + + test "retrieve local private metadata function documentation on __MODULE__ call" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + + def func(list) do + __MODULE__.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 10, 17) + + assert doc == %{ + args: ["list"], + arity: 1, + function: :flatten, + module: MyLocalModule, + metadata: %{since: "1.2.3"}, + specs: ["@spec flatten(list()) :: list()"], + docs: "Sample doc", + kind: :function + } + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "retrieve local private metadata function documentation on __MODULE__ submodule call" do + buffer = """ + defmodule MyLocalModule do + defmodule Sub do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + end + + def func(list) do + __MODULE__.Sub.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 12, 20) + + assert doc == %{ + args: ["list"], + function: :flatten, + arity: 1, + kind: :function, + metadata: %{since: "1.2.3"}, + module: MyLocalModule.Sub, + specs: ["@spec flatten(list()) :: list()"], + docs: "Sample doc" + } + end + end + + test "does not retrieve remote private metadata function documentation" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + defp flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + assert nil == Docs.docs(buffer, 12, 20) + end + + test "retrieve metadata function documentation with @doc false" do + buffer = """ + defmodule MyLocalModule do + @doc false + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 11, 20) + + assert doc == %{ + args: ["list"], + arity: 1, + function: :flatten, + kind: :function, + metadata: %{hidden: true}, + module: MyLocalModule, + specs: ["@spec flatten(list()) :: list()"], + docs: "" + } + end + + test "retrieve function documentation on @attr call" do + buffer = """ + defmodule MyModule do + @attr List + @attr.flatten(list) + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 12) + + assert %{ + args: ["list"], + function: :flatten, + module: List, + metadata: %{}, + specs: ["@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]"], + kind: :function + } = doc + + assert doc.docs =~ """ + Flattens the given `list` of nested lists. + """ + end + + test "retrieve erlang function documentation" do + buffer = """ + defmodule MyModule do + def func(list) do + :lists.flatten(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 12) + + assert %{ + args: ["deepList"], + function: :flatten, + module: :lists, + kind: :function + } = doc + + if System.otp_release() |> String.to_integer() >= 23 do + assert doc.docs =~ """ + Returns a flattened version of `DeepList`\\. + """ + + assert %{signature: _} = doc.metadata + end + end + + if System.otp_release() |> String.to_integer() >= 23 do + test "retrieve fallback erlang builtin function documentation" do + buffer = """ + defmodule MyModule do + def func(list) do + :erlang.or(a, b) + :erlang.orelse(a, b) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 14) + + assert %{ + arity: 2, + function: :or, + module: :erlang, + specs: ["@spec boolean() or boolean() :: boolean()"], + docs: "", + kind: :function + } = doc + + if String.to_integer(System.otp_release()) < 25 do + assert doc.args == ["boolean", "boolean"] + assert doc.metadata == %{app: :erts} + else + assert doc.args == ["term", "term"] + assert doc.metadata == %{hidden: true, app: :erts} + end + + %{ + docs: [doc] + } = Docs.docs(buffer, 4, 14) + + assert %{ + args: ["term", "term"], + arity: 2, + function: :orelse, + module: :erlang, + metadata: %{builtin: true, app: :erts}, + specs: [], + docs: "", + kind: :function + } = doc + end + end + + test "retrieve macro documentation" do + buffer = """ + defmodule MyModule do + require ElixirSenseExample.BehaviourWithMacrocallback.Impl, as: Macros + Macros.some({}) + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 12) + + assert doc == %{ + args: ["var"], + function: :some, + arity: 1, + module: ElixirSenseExample.BehaviourWithMacrocallback.Impl, + metadata: %{app: :language_server}, + specs: [ + "@spec some(integer()) :: Macro.t()\n@spec some(b) :: Macro.t() when b: float()" + ], + docs: "some macro\n", + kind: :macro + } + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "retrieve function documentation with __MODULE__ submodule call" do + buffer = """ + defmodule Inspect do + def func(list) do + __MODULE__.Algebra.string(list) + end + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 26) + + assert %{ + args: ["string"], + function: :string, + module: Inspect.Algebra, + metadata: %{since: "1.6.0"}, + specs: ["@spec string(String.t()) :: doc_string()"], + kind: :function + } = doc + + assert doc.docs =~ "Creates a document" + end + end + + test "retrieve function documentation from aliased modules" do + buffer = """ + defmodule MyModule do + alias List, as: MyList + MyList.flatten([]) + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 12) + + assert %{ + args: ["list"], + function: :flatten, + module: List, + metadata: %{}, + specs: ["@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]"], + kind: :function + } = doc + + assert doc.docs =~ """ + Flattens the given `list` of nested lists. + """ + end + + test "retrieve function documentation from imported modules" do + buffer = """ + defmodule MyModule do + import Mix.Generator + create_file("a", "b") + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 5) + + assert %{ + args: ["path", "contents", "opts \\\\ []"], + function: :create_file, + module: Mix.Generator, + metadata: %{defaults: 1}, + specs: ["@spec create_file(Path.t(), iodata(), keyword()) :: boolean()"], + kind: :function + } = doc + + assert doc.docs =~ "Creates a file with the given contents" + end + + test "find built-in functions" do + # module_info is defined by default for every elixir and erlang module + # __info__ is defined for every elixir module + # behaviour_info is defined for every behaviour and every protocol + buffer = """ + defmodule MyModule do + ElixirSenseExample.ModuleWithFunctions.module_info() + # ^ + ElixirSenseExample.ModuleWithFunctions.module_info(:exports) + # ^ + ElixirSenseExample.ModuleWithFunctions.__info__(:macros) + # ^ + ElixirSenseExample.ExampleBehaviour.behaviour_info(:callbacks) + # ^ + end + """ + + assert %{ + docs: [doc] + } = Docs.docs(buffer, 2, 42) + + assert %{ + args: [], + function: :module_info, + module: ElixirSenseExample.ModuleWithFunctions, + arity: 0, + metadata: %{builtin: true}, + specs: [ + "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]" + ], + docs: "The `module_info/0` function in each module" <> _, + kind: :function + } = doc + + assert %{ + docs: [doc] + } = Docs.docs(buffer, 4, 42) + + assert %{ + args: ["key"], + arity: 1, + function: :module_info, + module: ElixirSenseExample.ModuleWithFunctions, + metadata: %{builtin: true}, + specs: [ + "@spec module_info(:module) :: atom", + "@spec module_info(:attributes | :compile) :: [{atom, term}]", + "@spec module_info(:md5) :: binary", + "@spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}]", + "@spec module_info(:native) :: boolean" + ], + docs: "The call `module_info(Key)`" <> _, + kind: :function + } = doc + + assert %{docs: [%{function: :__info__}]} = + Docs.docs(buffer, 6, 42) + + assert %{docs: [%{function: :behaviour_info}]} = + Docs.docs(buffer, 8, 42) + end + + test "built-in functions cannot be called locally" do + # module_info is defined by default for every elixir and erlang module + # __info__ is defined for every elixir module + # behaviour_info is defined for every behaviour and every protocol + buffer = """ + defmodule MyModule do + import GenServer + @ callback cb() :: term + module_info() + #^ + __info__(:macros) + #^ + behaviour_info(:callbacks) + #^ + end + """ + + refute Docs.docs(buffer, 4, 5) + + refute Docs.docs(buffer, 6, 5) + + refute Docs.docs(buffer, 8, 5) + end + + test "retrieve function documentation from behaviour if available" do + buffer = """ + defmodule MyModule do + import ElixirSenseExample.ExampleBehaviourWithDocCallbackNoImpl + foo() + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 5) + + assert doc == %{ + args: [], + function: :foo, + arity: 0, + module: ElixirSenseExample.ExampleBehaviourWithDocCallbackNoImpl, + metadata: %{ + implementing: ElixirSenseExample.ExampleBehaviourWithDoc, + implementing_module_app: :language_server, + app: :language_server + }, + specs: ["@callback foo() :: :ok"], + docs: "Docs for foo", + kind: :function + } + end + + test "retrieve function documentation from behaviour even if @doc is set to false vie @impl" do + buffer = """ + defmodule MyModule do + import ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl + baz(1) + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 5) + + assert doc == %{ + args: ["a"], + function: :baz, + arity: 1, + module: ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl, + specs: ["@callback baz(integer()) :: :ok"], + metadata: %{ + implementing: ElixirSenseExample.ExampleBehaviourWithDoc, + hidden: true, + implementing_module_app: :language_server, + app: :language_server + }, + docs: "Docs for baz", + kind: :function + } + end + + test "retrieve function documentation from behaviour when callback has @doc false" do + buffer = """ + defmodule MyModule do + import ElixirSenseExample.ExampleBehaviourWithNoDocCallbackImpl + foo() + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 5) + + assert doc == %{ + args: [], + function: :foo, + arity: 0, + module: ElixirSenseExample.ExampleBehaviourWithNoDocCallbackImpl, + metadata: %{ + implementing: ElixirSenseExample.ExampleBehaviourWithNoDoc, + implementing_module_app: :language_server, + hidden: true, + app: :language_server + }, + specs: ["@callback foo() :: :ok"], + docs: "", + kind: :function + } + end + + test "retrieve macro documentation from behaviour if available" do + buffer = """ + defmodule MyModule do + import ElixirSenseExample.ExampleBehaviourWithDocCallbackNoImpl + bar(1) + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 5) + + assert doc == %{ + args: ["b"], + arity: 1, + function: :bar, + module: ElixirSenseExample.ExampleBehaviourWithDocCallbackNoImpl, + metadata: %{ + implementing: ElixirSenseExample.ExampleBehaviourWithDoc, + implementing_module_app: :language_server, + app: :language_server + }, + specs: ["@macrocallback bar(integer()) :: Macro.t()"], + docs: "Docs for bar", + kind: :macro + } + end + + if System.otp_release() |> String.to_integer() >= 25 do + test "retrieve erlang behaviour implementation" do + buffer = """ + :file_server.init(a) + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 1, 16) + + assert %{ + args: ["args"], + function: :init, + module: :file_server, + specs: ["@callback init(args :: term())" <> _], + metadata: %{implementing: :gen_server, implementing_module_app: :stdlib}, + kind: :function + } = doc + + assert doc.docs =~ "Whenever a `gen_server` process is started" + end + end + + test "do not crash for erlang behaviour callbacks" do + buffer = """ + defmodule MyModule do + import ElixirSenseExample.ExampleBehaviourWithDocCallbackErlang + init(:ok) + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 5) + + assert %{ + args: ["_"], + function: :init, + module: ElixirSenseExample.ExampleBehaviourWithDocCallbackErlang + } = doc + + if System.otp_release() |> String.to_integer() >= 23 do + assert doc.docs =~ "called by the new process" + + assert %{since: "OTP 19.0", implementing: :gen_statem, app: :language_server} = + doc.metadata + else + assert doc.docs == "" + assert doc.metadata == %{app: :language_server} + end + end + end + + describe "types" do + test "type with @typedoc false" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithDocs, as: Remote + @type my_list :: Remote.some_type_doc_false + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 31) + + assert doc == %{ + args: [], + arity: 0, + docs: "", + kind: :type, + metadata: %{hidden: true, app: :language_server}, + module: ElixirSenseExample.ModuleWithDocs, + spec: "@type some_type_doc_false() :: integer()", + type: :some_type_doc_false + } + end + + test "type no @typedoc" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithDocs, as: Remote + @type my_list :: Remote.some_type_no_doc + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 31) + + assert doc == %{ + args: [], + arity: 0, + docs: "", + kind: :type, + metadata: %{app: :language_server}, + module: ElixirSenseExample.ModuleWithDocs, + spec: "@type some_type_no_doc() :: integer()", + type: :some_type_no_doc + } + end + + test "retrieve type documentation" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithTypespecs.Remote + @type my_list :: Remote.remote_t + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 31) + + assert doc == %{ + args: [], + arity: 0, + docs: "Remote type", + kind: :type, + metadata: %{app: :language_server}, + module: ElixirSenseExample.ModuleWithTypespecs.Remote, + spec: "@type remote_t() :: atom()", + type: :remote_t + } + end + + test "retrieve metadata type documentation" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @type some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 8, 35) + + assert doc == %{ + args: ["a"], + type: :some, + arity: 1, + module: MyLocalModule, + metadata: %{since: "1.2.3"}, + spec: "@type some(a) :: {a}", + docs: "My example type", + kind: :type + } + end + + test "retrieve local private metadata type documentation" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @typep some(a) :: {a} + + @type my_list :: some(:a) + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 6, 22) + + assert doc == %{ + args: ["a"], + type: :some, + arity: 1, + module: MyLocalModule, + spec: "@typep some(a) :: {a}", + metadata: %{since: "1.2.3"}, + docs: "", + kind: :type + } + end + + test "retrieve local metadata type documentation even if it's defined after cursor" do + buffer = """ + defmodule MyModule do + @type remote_list_t :: [my_t] + # ^ + + @typep my_t :: integer + end + """ + + assert %{docs: [_]} = + Docs.docs(buffer, 2, 29) + end + + test "does not retrieve remote private metadata type documentation" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @typep some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + assert nil == Docs.docs(buffer, 8, 35) + end + + test "does not reveal details for opaque metadata type" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @opaque some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 8, 35) + + assert doc == %{ + args: ["a"], + type: :some, + arity: 1, + module: MyLocalModule, + spec: "@opaque some(a)", + metadata: %{since: "1.2.3", opaque: true}, + docs: "My example type", + kind: :type + } + end + + test "retrieve metadata type documentation with @typedoc false" do + buffer = """ + defmodule MyLocalModule do + @typedoc false + @type some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 7, 35) + + assert doc == %{ + args: ["a"], + type: :some, + arity: 1, + module: MyLocalModule, + spec: "@type some(a) :: {a}", + metadata: %{hidden: true}, + docs: "", + kind: :type + } + end + + test "does not reveal opaque type details" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.CallbackOpaque + @type my_list :: CallbackOpaque.t(integer) + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 35) + + assert doc == %{ + args: ["x"], + type: :t, + arity: 1, + module: ElixirSenseExample.CallbackOpaque, + spec: "@opaque t(x)", + metadata: %{opaque: true, app: :language_server}, + docs: "Opaque type\n", + kind: :type + } + end + + test "retrieve erlang type documentation" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithTypespecs.Remote + @type my_list :: :erlang.time_unit + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 3, 31) + + assert %{ + args: [], + type: :time_unit, + module: :erlang, + spec: "@type time_unit() ::\n pos_integer()" <> _, + kind: :type + } = doc + + if System.otp_release() |> String.to_integer() >= 23 do + assert doc.docs =~ """ + Supported time unit representations: + """ + + assert %{signature: _} = doc.metadata + end + end + + test "retrieve builtin type documentation" do + buffer = """ + defmodule MyModule do + @type options :: keyword + # ^ + @type options1 :: keyword(integer) + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 2, 23) + + assert doc == %{ + args: [], + type: :keyword, + arity: 0, + module: nil, + spec: "@type keyword() :: [{atom(), any()}]", + metadata: %{builtin: true}, + docs: "A keyword list", + kind: :type + } + + %{ + docs: [doc] + } = Docs.docs(buffer, 4, 23) + + assert doc == %{ + args: ["t"], + type: :keyword, + arity: 1, + module: nil, + metadata: %{builtin: true}, + spec: "@type keyword(t()) :: [{atom(), t()}]", + docs: "A keyword list with values of type `t`", + kind: :type + } + end + + test "retrieve basic type documentation" do + buffer = """ + defmodule MyModule do + @type num :: integer + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 2, 19) + + assert doc == %{ + args: [], + type: :integer, + module: nil, + arity: 0, + spec: "@type integer()", + metadata: %{builtin: true}, + docs: "An integer number", + kind: :type + } + end + + test "retrieve basic and builtin type documentation" do + buffer = """ + defmodule MyModule do + @type num :: list() + # ^ + @type num1 :: list(atom) + # ^ + end + """ + + %{ + docs: [doc] + } = Docs.docs(buffer, 2, 18) + + assert doc == %{ + args: [], + type: :list, + arity: 0, + module: nil, + spec: "@type list() :: [any()]", + metadata: %{builtin: true}, + docs: "A list", + kind: :type + } + + %{ + docs: [doc] + } = Docs.docs(buffer, 4, 18) + + assert doc == %{ + args: ["t"], + type: :list, + arity: 1, + module: nil, + spec: "@type list(t())", + metadata: %{builtin: true}, + docs: "Proper list ([]-terminated)", + kind: :type + } + end + end + + describe "attributes" do + test "retrieve reserved module attributes documentation" do + buffer = """ + defmodule MyModule do + @on_load :on_load + + def on_load(), do: :ok + end + """ + + assert %{ + docs: [doc] + } = Docs.docs(buffer, 2, 6) + + assert doc == %{ + name: "on_load", + docs: "A hook that will be invoked whenever the module is loaded.", + kind: :attribute + } + end + + test "retrieve unreserved module attributes documentation" do + buffer = """ + defmodule MyModule do + @my_attribute nil + end + """ + + assert %{ + docs: [doc] + } = Docs.docs(buffer, 2, 6) + + assert doc == %{name: "my_attribute", docs: "", kind: :attribute} + end + end + + test "retrieve docs on reserved words" do + buffer = """ + defmodule MyModule do + end + """ + + if Version.match?(System.version(), ">= 1.14.0") do + assert %{ + docs: [doc] + } = Docs.docs(buffer, 1, 21) + + assert doc == %{name: "do", docs: "do-end block control keyword", kind: :keyword} + else + assert nil == Docs.docs(buffer, 1, 21) + end + end + + describe "variables" do + test "retrieve docs on variables" do + buffer = """ + defmodule MyModule do + def fun(my_var) do + other_var = 5 + abc(my_var, other_var) + end + end + """ + + assert %{ + docs: [doc] + } = Docs.docs(buffer, 2, 12) + + assert doc == %{name: "my_var", kind: :variable} + + assert %{ + docs: [doc] + } = Docs.docs(buffer, 3, 6) + + assert doc == %{name: "other_var", kind: :variable} + end + + test "variables shadow builtin functions" do + buffer = """ + defmodule Vector do + @spec magnitude(Vec2.t()) :: number() + def magnitude(%Vec2{} = v), do: :math.sqrt(:math.pow(v.x, 2) + :math.pow(v.y, 2)) + + @spec normalize(Vec2.t()) :: Vec2.t() + def normalize(%Vec2{} = v) do + length = magnitude(v) + %{v | x: v.x / length, y: v.y / length} + end + end + """ + + assert %{ + docs: [%{kind: :variable}] + } = Docs.docs(buffer, 7, 6) + + assert %{ + docs: [%{kind: :variable}] + } = Docs.docs(buffer, 8, 21) + end + end + + test "find local type in typespec local def elsewhere" do + buffer = """ + defmodule ElixirSenseExample.Some do + @type some_local :: integer + + def some_local(), do: :ok + + @type user :: {some_local, integer} + + def foo do + some_local + end + end + """ + + assert %{docs: [%{kind: :type}]} = + Docs.docs(buffer, 6, 20) + + assert %{docs: [%{kind: :function}]} = + Docs.docs(buffer, 9, 9) + end + + describe "arity" do + test "retrieves documentation for correct arity function" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: {F.my_func(), F.my_func("a"), F.my_func(1, 2, 3), F.my_func(1, 2, 3, 4)} + end + """ + + assert %{docs: [doc]} = + Docs.docs(buffer, 3, 34) + + assert doc.docs =~ "2 params version" + + assert doc.specs == [ + "@spec my_func(1 | 2) :: binary()", + "@spec my_func(1 | 2, binary()) :: binary()" + ] + + # too many arguments + assert nil == Docs.docs(buffer, 3, 70) + end + + test "retrieves documentation for all matching arities with incomplete code" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func( + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 20) + + assert length(docs) == 3 + assert Enum.at(docs, 0).docs =~ "no params version" + assert Enum.at(docs, 1).docs =~ "2 params version" + assert Enum.at(docs, 2).docs =~ "3 params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func(1 + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 20) + + assert length(docs) == 2 + assert Enum.at(docs, 0).docs =~ "2 params version" + assert Enum.at(docs, 1).docs =~ "3 params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func(1, 2, + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 20) + + assert length(docs) == 1 + assert Enum.at(docs, 0).docs =~ "3 params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func(1, 2, 3 + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 20) + + assert length(docs) == 1 + assert Enum.at(docs, 0).docs =~ "3 params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: F.my_func(1, 2, 3, + end + """ + + # too many arguments + assert nil == Docs.docs(buffer, 3, 20) + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: 1 |> F.my_func( + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 26) + + assert length(docs) == 2 + assert Enum.at(docs, 0).docs =~ "2 params version" + assert Enum.at(docs, 1).docs =~ "3 params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def main, do: 1 |> F.my_func(1, + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 26) + + assert length(docs) == 1 + assert Enum.at(docs, 0).docs =~ "3 params version" + end + + test "retrieves documentation for correct arity function capture" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def go, do: &F.my_func/1 + end + """ + + assert %{docs: [doc]} = + Docs.docs(buffer, 3, 19) + + assert doc.docs =~ "2 params version" + + assert doc.specs == [ + "@spec my_func(1 | 2) :: binary()", + "@spec my_func(1 | 2, binary()) :: binary()" + ] + end + + test "retrieves documentation for correct arity type" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: {T.my_type, T.my_type(boolean), T.my_type(1, 2), T.my_type(1, 2, 3)} + end + """ + + assert %{docs: [doc]} = + Docs.docs(buffer, 3, 32) + + assert doc.docs =~ "one param version" + assert doc.spec == "@type my_type(a) :: {integer(), a}" + + # too many arguments + assert nil == Docs.docs(buffer, 3, 68) + end + + test "retrieves documentation for all matching type arities with incomplete code" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type( + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 20) + + assert length(docs) == 3 + assert Enum.at(docs, 0).docs =~ "no params version" + assert Enum.at(docs, 1).docs =~ "one param version" + assert Enum.at(docs, 2).docs =~ "two params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type(integer + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 20) + + assert length(docs) == 2 + assert Enum.at(docs, 0).docs =~ "one param version" + assert Enum.at(docs, 1).docs =~ "two params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type(integer, integer + end + """ + + assert %{docs: docs} = + Docs.docs(buffer, 3, 20) + + assert length(docs) == 1 + assert Enum.at(docs, 0).docs =~ "two params version" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.TypesWithMultipleArity, as: T + @type some :: T.my_type(integer, integer, + end + """ + + # too many arguments + assert nil == Docs.docs(buffer, 3, 20) + end + end +end diff --git a/apps/language_server/test/providers/hover_test.exs b/apps/language_server/test/providers/hover_test.exs index 55c552d27..2d3116fc5 100644 --- a/apps/language_server/test/providers/hover_test.exs +++ b/apps/language_server/test/providers/hover_test.exs @@ -5,7 +5,6 @@ defmodule ElixirLS.LanguageServer.Providers.HoverTest do alias ElixirLS.LanguageServer.Test.ParserContextBuilder alias ElixirLS.LanguageServer.Providers.Hover - # mix cmd --app language_server mix test test/providers/hover_test.exs def fake_dir() do Path.join(__DIR__, "../../../..") |> Path.expand() |> maybe_convert_path_separators() diff --git a/apps/language_server/test/providers/implementation/locator_test.exs b/apps/language_server/test/providers/implementation/locator_test.exs new file mode 100644 index 000000000..d467180ab --- /dev/null +++ b/apps/language_server/test/providers/implementation/locator_test.exs @@ -0,0 +1,726 @@ +defmodule ElixirLS.LanguageServer.Providers.Implementation.LocatorTest do + use ExUnit.Case, async: true + alias ElixirLS.LanguageServer.Providers.Implementation.Locator + alias ElixirLS.LanguageServer.Location + alias ElixirSense.Core.Source + + test "don't crash on empty buffer" do + assert [] == Locator.implementations("", 1, 1) + end + + test "don't error on __MODULE__ when no module" do + assert [] == Locator.implementations("__MODULE__", 1, 1) + end + + test "don't error on Elixir" do + assert [] == Locator.implementations("Elixir", 1, 1) + end + + test "don't error on not existing module" do + assert [] == Locator.implementations("SomeNotExistingMod", 1, 1) + end + + test "don't error on non behaviour module" do + assert [] == Locator.implementations("ElixirSenseExample.EmptyModule", 1, 32) + end + + test "don't error on erlang function calls" do + assert [] == Locator.implementations(":ets.new", 1, 8) + end + + test "don't return implementations for non callback functions on behaviour" do + assert [] == Locator.implementations("GenServer.start_link", 1, 12) + end + + test "don't error on non behaviour module function" do + buffer = """ + defmodule ElixirSenseExample.EmptyModule do + def abc(), do: :ok + end + """ + + assert [] == Locator.implementations(buffer, 2, 8) + end + + test "don't error on builtin macro" do + buffer = """ + defmodule ElixirSenseExample.EmptyModule do + def abc(), do: :ok + end + """ + + assert [] == Locator.implementations(buffer, 1, 8) + end + + test "find implementations of behaviour module" do + buffer = """ + defmodule ElixirSenseExample.ExampleBehaviourWithDoc do + end + """ + + [ + %Location{type: :module, file: file1, line: line1, column: column1}, + %Location{type: :module, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 1, 32) + + assert file1 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file1, {line1, column1}) =~ + "ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl" + + assert file2 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file2, {line2, column2}) =~ + "ElixirSenseExample.ExampleBehaviourWithDocCallbackNoImpl" + end + + test "find protocol implementations" do + buffer = """ + defprotocol ElixirSenseExample.ExampleProtocol do + end + """ + + [ + %Location{type: :module, file: file1, line: line1, column: column1}, + %Location{type: :module, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 1, 32) |> Enum.sort() + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "ElixirSenseExample.ExampleProtocol, for: List" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "ElixirSenseExample.ExampleProtocol, for: Map" + end + + test "find implementations of behaviour module callback" do + buffer = """ + defmodule ElixirSenseExample.ExampleBehaviourWithDoc do + @callback foo() :: :ok + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 2, 14) + + assert file1 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file1, {line1, column1}) =~ + "foo(), do: :ok" + + assert file2 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file2, {line2, column2}) =~ + "foo(), do: :ok" + end + + test "find implementations of behaviour module macrocallback" do + buffer = """ + defmodule ElixirSenseExample.ExampleBehaviourWithDoc do + @macrocallback bar(integer()) :: Macro.t() + end + """ + + [ + %Location{type: :macro, file: file1, line: line1, column: column1}, + %Location{type: :macro, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 2, 19) + + assert file1 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file1, {line1, column1}) =~ + "defmacro bar(_b)" + + assert file2 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file2, {line2, column2}) =~ + "defmacro bar(_b)" + end + + test "find implementations of behaviour module on callback in implementation" do + buffer = """ + defmodule Some do + @behaviour ElixirSenseExample.ExampleBehaviourWithDoc + def foo(), do: :ok + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2}, + %Location{type: :function, file: nil, line: 3, column: 3} + ] = Locator.implementations(buffer, 3, 8) + + assert file1 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file1, {line1, column1}) =~ + "foo(), do: :ok" + + assert file2 =~ "language_server/test/support/example_behaviour.ex" + + assert read_line(file2, {line2, column2}) =~ + "foo(), do: :ok" + end + + test "find implementations of metadata behaviour module callback" do + buffer = """ + defmodule MetadataBehaviour do + @callback foo() :: :ok + end + + defmodule Some do + @behaviour MetadataBehaviour + def foo(), do: :ok + end + """ + + [ + %Location{type: :function, file: nil, line: 7, column: 3} + ] = Locator.implementations(buffer, 2, 14) + end + + test "find implementations of metadata behaviour module macrocallback" do + buffer = """ + defmodule MetadataBehaviour do + @macrocallback foo(arg :: any) :: Macro.t + end + + defmodule Some do + @behaviour MetadataBehaviour + defmacro foo(arg), do: :ok + end + """ + + [ + %Location{type: :macro, file: nil, line: 7, column: 3} + ] = Locator.implementations(buffer, 2, 19) + end + + test "find implementations of metadata behaviour module macrocallback when implementation is a guard" do + buffer = """ + defmodule MetadataBehaviour do + @macrocallback foo(arg :: any) :: Macro.t + end + + defmodule Some do + @behaviour MetadataBehaviour + defguard foo(arg) when is_nil(arg) + end + """ + + [ + %Location{type: :macro, file: nil, line: 7, column: 3} + ] = Locator.implementations(buffer, 2, 19) + end + + test "find implementations of metadata behaviour" do + buffer = """ + defmodule MetadataBehaviour do + @callback foo() :: :ok + end + + defmodule Some do + @behaviour MetadataBehaviour + def foo(), do: :ok + end + """ + + [ + %Location{type: :module, file: nil, line: 5, column: 1} + ] = Locator.implementations(buffer, 1, 14) + end + + test "find protocol implementation functions" do + buffer = """ + defprotocol ElixirSenseExample.ExampleProtocol do + @spec some(t) :: any + def some(t) + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 3, 8) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + end + + test "find protocol implementation functions on spec" do + buffer = """ + defprotocol ElixirSenseExample.ExampleProtocol do + @spec some(t) :: any + def some(t) + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 2, 10) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + end + + test "find metadata protocol implementation functions on spec" do + buffer = """ + defprotocol MetadataProtocol do + @spec some(t) :: any + def some(t) + end + + defimpl MetadataProtocol, for: String do + def some(t), do: :ok + end + """ + + [ + %Location{type: :function, file: nil, line: 7, column: 3} + ] = Locator.implementations(buffer, 2, 10) + end + + test "find protocol implementation functions on implementation function" do + buffer = """ + defimpl ElixirSenseExample.ExampleProtocol, for: String do + def some(t), do: t + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2}, + %Location{type: :function, file: nil, line: 2, column: 3} + ] = Locator.implementations(buffer, 2, 8) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + end + + test "find metadata protocol implementation functions on function" do + buffer = """ + defprotocol MetadataProtocol do + @spec some(t) :: any + def some(t) + end + + defimpl MetadataProtocol, for: String do + def some(t), do: :ok + end + """ + + [ + %Location{type: :function, file: nil, line: 7, column: 3} + ] = Locator.implementations(buffer, 3, 8) + end + + test "find metadata protocol implementation functions on function when implementation is a delegate" do + buffer = """ + defprotocol MetadataProtocol do + @spec some(t) :: any + def some(t) + end + + defimpl MetadataProtocol, for: String do + defdelegate some(t), to: Impl + end + """ + + [ + %Location{type: :function, file: nil, line: 7, column: 3} + ] = Locator.implementations(buffer, 3, 8) + end + + test "find metadata protocol implementation" do + buffer = """ + defprotocol MetadataProtocol do + @spec some(t) :: any + def some(t) + end + + defimpl MetadataProtocol, for: String do + def some(t), do: :ok + end + """ + + [ + %Location{type: :module, file: nil, line: 6, column: 1} + ] = Locator.implementations(buffer, 1, 14) + end + + test "find protocol implementation functions on implementation function - incomplete code" do + buffer = """ + defimpl ElixirSenseExample.ExampleProtocol, for: String do + def some(t + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2}, + %Location{type: :function, file: nil, line: 2, column: 3} + ] = Locator.implementations(buffer, 2, 8) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + + buffer = """ + defimpl ElixirSenseExample.ExampleProtocol, for: String do + def some(t, 1, + end + """ + + # too many arguments + + assert [] = Locator.implementations(buffer, 2, 8) + end + + test "find protocol implementation functions on call" do + buffer = """ + ElixirSenseExample.ExampleProtocol.some(1) + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 1, 37) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + end + + test "find protocol implementation functions on call with incomplete code" do + buffer = """ + ElixirSenseExample.ExampleProtocol.some( + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 1, 37) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + + buffer = """ + ElixirSenseExample.ExampleProtocol.some(a, + """ + + # too many arguments + + assert [] = Locator.implementations(buffer, 1, 37) + end + + test "find protocol implementation functions on call with alias" do + buffer = """ + defmodule Some do + alias ElixirSenseExample.ExampleProtocol, as: A + A.some(1) + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 3, 6) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + end + + test "find protocol implementation functions on call via @attr" do + buffer = """ + defmodule Some do + @attr ElixirSenseExample.ExampleProtocol + @attr.some(1) + end + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2} + ] = Locator.implementations(buffer, 3, 10) + + assert file1 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file1, {line1, column1}) =~ "some(t), do: t" + + assert file2 =~ "language_server/test/support/example_protocol.ex" + assert read_line(file2, {line2, column2}) =~ "some(t), do: t" + end + + test "find behaviour implementation functions on call" do + buffer = """ + ElixirSenseExample.DummyBehaviourImplementation.foo() + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1} + ] = Locator.implementations(buffer, 1, 49) + + assert file1 =~ "language_server/test/support/behaviour_implementations.ex" + assert read_line(file1, {line1, column1}) =~ "def foo(), do: :ok" + end + + test "find behaviour implementation functions on call metadata" do + buffer = """ + defmodule Some do + @behaviour ElixirSenseExample.ExampleBehaviourWithDoc + def foo(), do: :ok + end + + Some.foo() + """ + + [ + %Location{type: :function, file: file1, line: line1, column: column1}, + %Location{type: :function, file: file2, line: line2, column: column2}, + %Location{type: :function, file: nil, line: 3, column: 3} + ] = Locator.implementations(buffer, 6, 7) + + assert file1 =~ "language_server/test/support/example_behaviour.ex" + assert read_line(file1, {line1, column1}) =~ "def foo(), do: :ok" + + assert file2 =~ "language_server/test/support/example_behaviour.ex" + assert read_line(file2, {line2, column2}) =~ "def foo(), do: :ok" + end + + test "find behaviour macrocallback implementation functions on call metadata" do + buffer = """ + defmodule Some do + @behaviour ElixirSenseExample.ExampleBehaviourWithDoc + defmacro bar(a), do: :ok + end + + Some.bar() + Some.bar(1) + Some.bar(1, 2) + """ + + [ + %Location{type: :macro, file: file1, line: line1, column: column1}, + %Location{type: :macro, file: file2, line: line2, column: column2}, + %Location{type: :macro, file: nil, line: 3, column: 3} + ] = Locator.implementations(buffer, 7, 7) + + assert file1 =~ "language_server/test/support/example_behaviour.ex" + assert read_line(file1, {line1, column1}) =~ "defmacro bar(_b)" + + assert file2 =~ "language_server/test/support/example_behaviour.ex" + assert read_line(file2, {line2, column2}) =~ "defmacro bar(_b)" + + # too little arguments + + [] = Locator.implementations(buffer, 6, 7) + + # too many arguments + + [] = Locator.implementations(buffer, 8, 7) + end + + test "find implementation of delegated functions" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + MyMod.delegated_function() + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 3, 11) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "delegated_function do" + end + + test "find implementation of delegated functions in incomplete code" do + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + MyMod.delegated_function( + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 3, 11) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "delegated_function do" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + MyMod.delegated_function(1 + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 3, 11) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "delegated_function(a) do" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + MyMod.delegated_function(1, + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 3, 11) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "delegated_function(a, b) do" + + buffer = """ + defmodule MyModule do + alias ElixirSenseExample.ModuleWithFunctions, as: MyMod + MyMod.delegated_function(1, 2, + # ^ + end + """ + + # too many arguments + + assert [] = Locator.implementations(buffer, 3, 11) + end + + test "find implementation of delegated functions via @attr" do + buffer = """ + defmodule MyModule do + @attr ElixirSenseExample.ModuleWithFunctions + def a do + @attr.delegated_function() + end + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 4, 13) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "delegated_function do" + end + + test "handle defdelegate" do + buffer = """ + defmodule MyModule do + defdelegate delegated_function, to: ElixirSenseExample.ModuleWithFunctions.DelegatedModule + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 2, 15) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "def delegated_function do" + end + + test "handle defdelegate - navigate to correct arity" do + buffer = """ + defmodule MyModule do + defdelegate delegated_function(a), to: ElixirSenseExample.ModuleWithFunctions.DelegatedModule + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 2, 15) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "def delegated_function(a) do" + end + + test "handle defdelegate - navigate to correct arity on default args" do + buffer = """ + defmodule MyModule do + defdelegate delegated_function(a \\\\ nil), to: ElixirSenseExample.ModuleWithFunctions.DelegatedModule + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 2, 15) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "def delegated_function(a) do" + end + + test "handle defdelegate with `as`" do + buffer = """ + defmodule MyModule do + defdelegate my_function, to: ElixirSenseExample.ModuleWithFunctions.DelegatedModule, as: :delegated_function + # ^ + end + """ + + [%Location{type: :function, file: file, line: line, column: column}] = + Locator.implementations(buffer, 2, 15) + + assert file =~ "language_server/test/support/module_with_functions.ex" + assert read_line(file, {line, column}) =~ "delegated_function" + end + + test "defdelegate to metadata module" do + buffer = """ + defmodule SomeModWithDelegatee do + def delegated_function, do: :ok + end + + defmodule MyModule do + defdelegate delegated_function, to: SomeModWithDelegatee + # ^ + end + """ + + assert [%Location{type: :function, file: nil, line: 2, column: 3}] == + Locator.implementations(buffer, 6, 15) + end + + test "handle recursion in defdelegate" do + buffer = """ + defmodule MyModule do + defdelegate delegated_function, to: MyModule + # ^ + end + """ + + assert [] == Locator.implementations(buffer, 2, 15) + end + + defp read_line(file, {line, column}) do + file + |> File.read!() + |> Source.split_lines() + |> Enum.at(line - 1) + |> String.slice((column - 1)..-1//1) + end +end diff --git a/apps/language_server/test/providers/references/locator_test.exs b/apps/language_server/test/providers/references/locator_test.exs new file mode 100644 index 000000000..1344fb1f9 --- /dev/null +++ b/apps/language_server/test/providers/references/locator_test.exs @@ -0,0 +1,1906 @@ +defmodule ElixirLS.LanguageServer.Providers.References.LocatorTest do + use ExUnit.Case, async: true + # TODO remove + alias ElixirSense.Core.References.Tracer + # TODO remove + alias ElixirSense.Core.Source + alias ElixirLS.LanguageServer.Providers.References.Locator + + setup_all do + {:ok, _} = Tracer.start_link() + + Code.compiler_options( + tracers: [Tracer], + ignore_module_conflict: true, + parser_options: [columns: true] + ) + + Code.compile_file("./test/support/modules_with_references.ex") + Code.compile_file("./test/support/module_with_builtin_type_shadowing.ex") + Code.compile_file("./test/support/subscriber.ex") + Code.compile_file("./test/support/functions_with_default_args.ex") + + trace = Tracer.get() + + %{trace: trace} + end + + test "finds reference to local function shadowing builtin type", %{trace: trace} do + buffer = """ + defmodule B.Callee do + def fun() do + # ^ + :ok + end + def my_fun() do + :ok + end + end + """ + + references = Locator.references(buffer, 2, 8, trace) + + assert [ + %{ + range: range_1, + uri: "test/support/module_with_builtin_type_shadowing.ex" + } + ] = references + + assert range_1 == %{start: %{column: 14, line: 4}, end: %{column: 17, line: 4}} |> maybe_shift + end + + test "find references with cursor over a function call", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee1.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + assert [ + %{range: %{end: %{column: 62, line: 3}, start: %{column: 58, line: 3}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_3 + } + ] = references + + assert range_1 == + %{start: %{line: 36, column: 60}, end: %{line: 36, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 16}, end: %{line: 65, column: 20}} |> maybe_shift + + assert range_3 == + %{start: %{line: 65, column: 63}, end: %{line: 65, column: 67}} |> maybe_shift + end + + test "find references with cursor over a function definition", %{trace: trace} do + buffer = """ + defmodule ElixirSense.Providers.ReferencesTest.Modules.Callee1 do + def func() do + # ^ + IO.puts "" + end + def func(par1) do + # ^ + IO.puts par1 + end + end + """ + + references = Locator.references(buffer, 2, 10, trace) + + assert [ + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_3 + } + ] = references + + assert range_1 == + %{start: %{line: 36, column: 60}, end: %{line: 36, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 16}, end: %{line: 65, column: 20}} |> maybe_shift + + assert range_3 == + %{start: %{line: 65, column: 63}, end: %{line: 65, column: 67}} |> maybe_shift + + references = Locator.references(buffer, 6, 10, trace) + + assert [ + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + } + ] = references + + assert range_1 == + %{start: %{line: 42, column: 60}, end: %{line: 42, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 79}, end: %{line: 65, column: 83}} |> maybe_shift + end + + test "find references with cursor over a function definition with default arg", %{trace: trace} do + buffer = """ + defmodule ElixirSenseExample.Subscription do + def check(resource, models, user, opts \\\\ []) do + IO.inspect({resource, models, user, opts}) + end + end + """ + + references = Locator.references(buffer, 2, 10, trace) + + assert [ + %{ + range: range_1, + uri: "test/support/subscriber.ex" + }, + %{ + range: range_2, + uri: "test/support/subscriber.ex" + } + ] = references + + assert range_1 == %{end: %{column: 42, line: 3}, start: %{column: 37, line: 3}} |> maybe_shift + assert range_2 == %{end: %{column: 42, line: 4}, start: %{column: 37, line: 4}} |> maybe_shift + end + + test "find references with cursor over a function with arity 1", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee1.func("test") + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + assert [ + %{range: %{end: %{column: 62, line: 3}, start: %{column: 58, line: 3}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + } + ] = references + + assert range_1 == + %{start: %{line: 42, column: 60}, end: %{line: 42, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 79}, end: %{line: 65, column: 83}} |> maybe_shift + end + + test "find references with cursor over a function called via @attr.call", %{trace: trace} do + buffer = """ + defmodule Caller do + @attr ElixirSense.Providers.ReferencesTest.Modules.Callee1 + def func() do + @attr.func("test") + # ^ + end + end + """ + + references = Locator.references(buffer, 4, 12, trace) + + assert [ + %{range: %{end: %{column: 15, line: 4}, start: %{column: 11, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + } + ] = references + + assert range_1 == + %{start: %{line: 42, column: 60}, end: %{line: 42, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 79}, end: %{line: 65, column: 83}} |> maybe_shift + end + + # TODO crashes metadata builder + # test "find references with cursor over a function called via @attr.Submodule.call", %{trace: trace} do + # buffer = """ + # defmodule Caller do + # @attr ElixirSense.Providers.ReferencesTest.Modules + # def func() do + # @attr.Callee1.func("test") + # # ^ + # end + # end + # """ + + # references = Locator.references(buffer, 4, 20, trace) + + # assert [ + # %{range: %{end: %{column: 15, line: 4}, start: %{column: 11, line: 4}}, uri: nil}, + # %{ + # uri: "test/support/modules_with_references.ex", + # range: range_1 + # }, + # %{ + # uri: "test/support/modules_with_references.ex", + # range: range_2 + # } + # ] = references + + # assert range_1 == %{start: %{line: 42, column: 60}, end: %{line: 42, column: 64}} + # assert range_2 == %{start: %{line: 65, column: 79}, end: %{line: 65, column: 83}} + # end + + test "find references to function called via @attr.call", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee7.func_noarg() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + assert [ + %{ + range: %{end: %{column: 68, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/modules_with_references.ex" + } + ] = references + + assert range_1 == + %{end: %{column: 23, line: 114}, start: %{column: 13, line: 114}} |> maybe_shift + end + + test "find references with cursor over a function with arity 1 called via pipe operator", %{ + trace: trace + } do + buffer = """ + defmodule Caller do + def func() do + "test" + |> ElixirSense.Providers.ReferencesTest.Modules.Callee4.func_arg() + # ^ + end + end + """ + + references = Locator.references(buffer, 4, 62, trace) + + assert [ + %{ + range: %{end: %{column: 69, line: 4}, start: %{column: 61, line: 4}}, + uri: nil + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + } + ] = references + + assert range_1 == + %{start: %{line: 49, column: 63}, end: %{line: 49, column: 71}} |> maybe_shift + end + + test "find references with cursor over a function with arity 1 captured", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + Task.start(&ElixirSense.Providers.ReferencesTest.Modules.Callee4.func_arg/1) + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 72, trace) + + assert [ + %{ + range: %{end: %{column: 78, line: 3}, start: %{column: 70, line: 3}}, + uri: nil + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + } + ] = references + + assert range_1 == + %{start: %{line: 49, column: 63}, end: %{line: 49, column: 71}} |> maybe_shift + end + + test "find references with cursor over a function when caller uses pipe operator", %{ + trace: trace + } do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee4.func_arg("test") + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + assert [ + %{ + range: %{end: %{column: 66, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + } + ] = references + + assert range_1 == + %{start: %{line: 49, column: 63}, end: %{line: 49, column: 71}} |> maybe_shift + end + + test "find references with cursor over a function when caller uses capture operator", %{ + trace: trace + } do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee4.func_no_arg() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + assert [ + %{ + range: %{end: %{column: 69, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range + } + ] = references + + if Version.match?(System.version(), ">= 1.14.0-rc.0") do + # before 1.14 tracer reports invalid positions for captures + # https://github.com/elixir-lang/elixir/issues/12023 + assert range == %{start: %{line: 55, column: 72}, end: %{line: 55, column: 83}} + end + end + + test "find references with cursor over a function with default argument when caller uses default arguments", + %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee5.func_arg() + ElixirSense.Providers.ReferencesTest.Modules.Callee5.func_arg("test") + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + assert [ + %{ + range: %{end: %{column: 66, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{range: %{end: %{column: 66, line: 4}, start: %{column: 58, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + } + ] = references + + assert range_1 == + %{start: %{line: 90, column: 60}, end: %{line: 90, column: 68}} |> maybe_shift + + references = Locator.references(buffer, 4, 59, trace) + + assert [ + %{ + range: %{end: %{column: 66, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{range: %{end: %{column: 66, line: 4}, start: %{column: 58, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + } + ] = references + + assert range_1 == + %{start: %{line: 90, column: 60}, end: %{line: 90, column: 68}} |> maybe_shift + end + + test "find references with cursor over a function with default argument when caller does not uses default arguments", + %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee5.func_arg1("test") + ElixirSense.Providers.ReferencesTest.Modules.Callee5.func_arg1() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + assert [ + %{ + range: %{end: %{column: 67, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{range: %{end: %{column: 67, line: 4}, start: %{column: 58, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + } + ] = references + + assert range_1 == + %{start: %{line: 91, column: 60}, end: %{line: 91, column: 69}} |> maybe_shift + + references = Locator.references(buffer, 4, 59, trace) + + assert [ + %{ + range: %{end: %{column: 67, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{range: %{end: %{column: 67, line: 4}, start: %{column: 58, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + } + ] = references + + assert range_1 == + %{start: %{line: 91, column: 60}, end: %{line: 91, column: 69}} |> maybe_shift + end + + test "find references with cursor over a module with funs with default argument", %{ + trace: trace + } do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee5.func_arg1("test") + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 55, trace) + + assert [ + %{range: %{end: %{column: 67, line: 3}, start: %{column: 58, line: 3}}, uri: nil}, + %{ + range: range_1, + uri: "test/support/modules_with_references.ex" + }, + %{ + range: range_2, + uri: "test/support/modules_with_references.ex" + } + ] = references + + assert range_1 == + %{end: %{column: 68, line: 90}, start: %{column: 60, line: 90}} |> maybe_shift + + assert range_2 == + %{end: %{column: 69, line: 91}, start: %{column: 60, line: 91}} |> maybe_shift + end + + test "find references for the correct arity version", %{trace: trace} do + buffer = """ + defmodule Caller do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def func() do + F.my_func(1) + F.my_func(1, "") + F.my_func() + F.my_func(1, 2, 3) + end + end + """ + + references = Locator.references(buffer, 4, 8, trace) + + assert [ + %{ + range: %{end: %{column: 14, line: 4}, start: %{column: 7, line: 4}}, + uri: nil + }, + %{ + range: %{end: %{column: 14, line: 5}, start: %{column: 7, line: 5}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/functions_with_default_args.ex" + }, + %{ + range: range_2, + uri: "test/support/functions_with_default_args.ex" + } + ] = references + + assert read_line("test/support/functions_with_default_args.ex", range_1) =~ "my_func(1)" + + assert read_line("test/support/functions_with_default_args.ex", range_2) =~ + "my_func(1, \"a\")" + + references = Locator.references(buffer, 5, 8, trace) + + assert [ + %{ + range: %{end: %{column: 14, line: 4}, start: %{column: 7, line: 4}}, + uri: nil + }, + %{ + range: %{end: %{column: 14, line: 5}, start: %{column: 7, line: 5}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/functions_with_default_args.ex" + }, + %{ + range: range_2, + uri: "test/support/functions_with_default_args.ex" + } + ] = references + + assert read_line("test/support/functions_with_default_args.ex", range_1) =~ "my_func(1)" + + assert read_line("test/support/functions_with_default_args.ex", range_2) =~ + "my_func(1, \"a\")" + end + + test "find references for the correct arity version in incomplete code", %{trace: trace} do + buffer = """ + defmodule Caller do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def func() do + F.my_func( + end + end + """ + + references = Locator.references(buffer, 4, 8, trace) + + assert [ + %{ + range: %{end: %{column: 14, line: 4}, start: %{column: 7, line: 4}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/functions_with_default_args.ex" + }, + %{ + range: range_2 + }, + %{ + range: range_3 + }, + %{ + range: range_4 + } + ] = references + + assert read_line("test/support/functions_with_default_args.ex", range_1) =~ "my_func()" + assert read_line("test/support/functions_with_default_args.ex", range_2) =~ "my_func(1)" + + assert read_line("test/support/functions_with_default_args.ex", range_3) =~ + "my_func(1, \"a\")" + + assert read_line("test/support/functions_with_default_args.ex", range_4) =~ "my_func(1, 2, 3)" + + buffer = """ + defmodule Caller do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def func() do + F.my_func(1 + end + end + """ + + references = Locator.references(buffer, 4, 8, trace) + + assert [ + %{ + range: %{end: %{column: 14, line: 4}, start: %{column: 7, line: 4}}, + uri: nil + }, + %{ + range: range_2, + uri: "test/support/functions_with_default_args.ex" + }, + %{ + range: range_3 + }, + %{ + range: range_4 + } + ] = references + + assert read_line("test/support/functions_with_default_args.ex", range_2) =~ "my_func(1)" + + assert read_line("test/support/functions_with_default_args.ex", range_3) =~ + "my_func(1, \"a\")" + + assert read_line("test/support/functions_with_default_args.ex", range_4) =~ "my_func(1, 2, 3)" + + buffer = """ + defmodule Caller do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def func() do + F.my_func(1, 2, + end + end + """ + + references = Locator.references(buffer, 4, 8, trace) + + assert [ + %{ + range: %{end: %{column: 14, line: 4}, start: %{column: 7, line: 4}}, + uri: nil + }, + %{ + range: range_4, + uri: "test/support/functions_with_default_args.ex" + } + ] = references + + assert read_line("test/support/functions_with_default_args.ex", range_4) =~ "my_func(1, 2, 3)" + + buffer = """ + defmodule Caller do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + def func() do + F.my_func(1, 2, 3, + end + end + """ + + references = Locator.references(buffer, 4, 8, trace) + + assert [] == references + end + + test "find references for the correct arity version for metadata calls", %{trace: trace} do + buffer = """ + defmodule SomeCallee do + def my_func(), do: :ok + def my_func(a, b \\\\ ""), do: :ok + def my_func(1, 2, 3), do: :ok + end + + defmodule Caller do + alias SomeCallee, as: F + def func() do + F.my_func(1) + F.my_func(1, "") + F.my_func() + F.my_func(1, 2, 3) + end + end + """ + + references = Locator.references(buffer, 3, 8, trace) + + assert [ + %{ + range: %{ + end: %{column: 14, line: 10}, + start: %{column: 7, line: 10} + }, + uri: nil + }, + %{ + range: %{ + end: %{column: 14, line: 11}, + start: %{column: 7, line: 11} + }, + uri: nil + } + ] = references + + references = Locator.references(buffer, 10, 8, trace) + + assert [ + %{ + range: %{ + end: %{column: 14, line: 10}, + start: %{column: 7, line: 10} + }, + uri: nil + }, + %{ + range: %{ + end: %{column: 14, line: 11}, + start: %{column: 7, line: 11} + }, + uri: nil + } + ] = references + end + + test "does not find references for private remote calls in metadata", %{trace: trace} do + buffer = """ + defmodule SomeCallee do + defp my_func(), do: :ok + defp my_func(a, b \\\\ ""), do: :ok + defp my_func(1, 2, 3), do: :ok + end + + defmodule Caller do + alias SomeCallee, as: F + def func() do + F.my_func(1) + F.my_func(1, "") + F.my_func() + F.my_func(1, 2, 3) + end + end + """ + + references = Locator.references(buffer, 3, 9, trace) + + assert [] == references + + references = Locator.references(buffer, 10, 8, trace) + + assert [] == references + end + + test "find references for metadata calls on variable or attribute", + %{trace: trace} do + buffer = """ + defmodule A do + @callback abc() :: any() + end + + defmodule B do + @behaviour A + + def abc, do: :ok + end + + defmodule X do + @b B + @b.abc() + def a do + b = B + b.abc() + end + end + """ + + references = Locator.references(buffer, 8, 8, trace) + + assert [ + %{ + range: %{ + end: %{column: 9, line: 13}, + start: %{column: 6, line: 13} + }, + uri: nil + }, + %{ + range: %{ + end: %{column: 10, line: 16}, + start: %{column: 7, line: 16} + }, + uri: nil + } + ] = references + end + + test "find references for the correct arity version for metadata calls with cursor over module", + %{trace: trace} do + buffer = """ + defmodule SomeCallee do + def my_func(), do: :ok + def my_func(a, b \\\\ ""), do: :ok + def my_func(1, 2, 3), do: :ok + end + + defmodule Caller do + alias SomeCallee, as: F + def func() do + F.my_func(1) + F.my_func(1, "") + F.my_func() + F.my_func(1, 2, 3) + end + end + """ + + references = Locator.references(buffer, 1, 13, trace) + + assert [ + %{ + range: %{ + end: %{column: 14, line: 10}, + start: %{column: 7, line: 10} + }, + uri: nil + }, + %{ + range: %{ + end: %{column: 14, line: 11}, + start: %{column: 7, line: 11} + }, + uri: nil + }, + %{range: %{end: %{column: 14, line: 12}, start: %{column: 7, line: 12}}, uri: nil}, + %{range: %{end: %{column: 14, line: 13}, start: %{column: 7, line: 13}}, uri: nil} + ] = references + + references = Locator.references(buffer, 10, 8, trace) + + assert [ + %{ + range: %{ + end: %{column: 14, line: 10}, + start: %{column: 7, line: 10} + }, + uri: nil + }, + %{ + range: %{ + end: %{column: 14, line: 11}, + start: %{column: 7, line: 11} + }, + uri: nil + } + ] = references + end + + test "find references with cursor over a module with multi alias syntax", %{trace: trace} do + buffer = """ + defmodule Caller do + alias ElixirSense.Providers.ReferencesTest.Modules.Callee5 + alias ElixirSense.Providers.ReferencesTest.Modules.{Callee5} + end + """ + + references_1 = Locator.references(buffer, 2, 57, trace) + references_2 = Locator.references(buffer, 3, 58, trace) + + assert references_1 == references_2 + assert [_, _] = references_1 + end + + test "find references with cursor over a function call from an aliased module", %{trace: trace} do + buffer = """ + defmodule Caller do + def my() do + alias ElixirSense.Providers.ReferencesTest.Modules.Callee1, as: C + C.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 4, 8, trace) + + assert [ + %{range: %{end: %{column: 11, line: 4}, start: %{column: 7, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_3 + } + ] = references + + assert range_1 == + %{start: %{line: 36, column: 60}, end: %{line: 36, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 16}, end: %{line: 65, column: 20}} |> maybe_shift + + assert range_3 == + %{start: %{line: 65, column: 63}, end: %{line: 65, column: 67}} |> maybe_shift + end + + test "find references with cursor over a function call from an imported module", %{trace: trace} do + buffer = """ + defmodule Caller do + def my() do + import ElixirSense.Providers.ReferencesTest.Modules.Callee1 + func() + #^ + end + end + """ + + references = Locator.references(buffer, 4, 6, trace) + + assert [ + %{range: %{end: %{column: 9, line: 4}, start: %{column: 5, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_3 + } + ] = references + + assert range_1 == + %{start: %{line: 36, column: 60}, end: %{line: 36, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 16}, end: %{line: 65, column: 20}} |> maybe_shift + + assert range_3 == + %{start: %{line: 65, column: 63}, end: %{line: 65, column: 67}} |> maybe_shift + end + + test "find references with cursor over a function call pipe from an imported module", %{ + trace: trace + } do + buffer = """ + defmodule Caller do + def my() do + import ElixirSense.Providers.ReferencesTest.Modules.Callee1 + "" |> func + # ^ + end + end + """ + + references = Locator.references(buffer, 4, 12, trace) + + assert [ + %{range: %{end: %{column: 15, line: 4}, start: %{column: 11, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + } + ] = references + + assert range_1 == + %{start: %{line: 42, column: 60}, end: %{line: 42, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 79}, end: %{line: 65, column: 83}} |> maybe_shift + end + + test "find references with cursor over a function capture from an imported module", %{ + trace: trace + } do + buffer = """ + defmodule Caller do + def my() do + import ElixirSense.Providers.ReferencesTest.Modules.Callee1 + &func/0 + # ^ + end + end + """ + + references = Locator.references(buffer, 4, 7, trace) + + assert [ + %{range: %{end: %{column: 10, line: 4}, start: %{column: 6, line: 4}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_3 + } + ] = references + + assert range_1 == + %{start: %{line: 36, column: 60}, end: %{line: 36, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 65, column: 16}, end: %{line: 65, column: 20}} |> maybe_shift + + assert range_3 == + %{start: %{line: 65, column: 63}, end: %{line: 65, column: 67}} |> maybe_shift + end + + test "find imported references", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee3.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + if Version.match?(System.version(), ">= 1.13.0") do + assert references == [ + %{ + range: %{end: %{column: 62, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{ + uri: "test/support/modules_with_references.ex", + range: %{start: %{line: 65, column: 47}, end: %{line: 65, column: 51}} + }, + %{ + range: %{end: %{column: 13, line: 70}, start: %{column: 9, line: 70}}, + uri: "test/support/modules_with_references.ex" + } + ] + end + end + + test "find references from remote calls with the function in the next line", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee3.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 59, trace) + + if Version.match?(System.version(), ">= 1.13.0") do + assert [ + %{ + range: %{end: %{column: 62, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{ + range: %{end: %{column: 51, line: 65}, start: %{column: 47, line: 65}}, + uri: "test/support/modules_with_references.ex" + }, + %{ + range: %{end: %{column: 13, line: 70}, start: %{column: 9, line: 70}}, + uri: "test/support/modules_with_references.ex" + } + ] = references + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "find references when module with __MODULE__ special form submodule function", %{ + trace: trace + } do + buffer = """ + defmodule ElixirSense.Providers.ReferencesTest.Modules do + def func() do + __MODULE__.Callee3.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 25, trace) + + assert references == [ + %{range: %{end: %{column: 28, line: 3}, start: %{column: 24, line: 3}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: %{start: %{line: 65, column: 47}, end: %{line: 65, column: 51}} + }, + %{ + range: %{end: %{column: 13, line: 70}, start: %{column: 9, line: 70}}, + uri: "test/support/modules_with_references.ex" + } + ] + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "find references when module with __MODULE__ special form submodule", %{trace: trace} do + buffer = """ + defmodule MyLocalModule do + defmodule Some do + def func() do + :ok + end + end + __MODULE__.Some.func() + end + """ + + references = Locator.references(buffer, 7, 15, trace) + + assert references == [ + %{range: %{start: %{column: 19, line: 7}, end: %{column: 23, line: 7}}, uri: nil} + ] + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "find references when module with __MODULE__ special form function", %{trace: trace} do + buffer = """ + defmodule ElixirSense.Providers.ReferencesTest.Modules do + def func() do + __MODULE__.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 18, trace) + + assert references == [ + %{ + uri: nil, + range: %{ + end: %{column: 20, line: 3}, + start: %{column: 16, line: 3} + } + } + ] + end + end + + test "find references when module with __MODULE__ special form", %{trace: trace} do + buffer = """ + defmodule MyLocalModule do + def func() do + __MODULE__.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 10, trace) + + assert references == [ + %{ + uri: nil, + range: %{ + end: %{column: 20, line: 3}, + start: %{column: 16, line: 3} + } + } + ] + end + + test "find references of variables", %{trace: trace} do + buffer = """ + defmodule MyModule do + def func do + var1 = 1 + var2 = 2 + var1 = 3 + IO.puts(var1 + var2) + end + def func4(ppp) do + + end + end + """ + + references = Locator.references(buffer, 6, 13, trace) + + assert references == [ + %{uri: nil, range: %{start: %{line: 5, column: 5}, end: %{line: 5, column: 9}}}, + %{uri: nil, range: %{start: %{line: 6, column: 13}, end: %{line: 6, column: 17}}} + ] + + references = Locator.references(buffer, 3, 6, trace) + + assert references == [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 9}}} + ] + end + + test "find references of variables outside module", %{trace: trace} do + buffer = """ + bas = B + bas.abc() + """ + + references = Locator.references(buffer, 1, 2, trace) + + assert references == [ + %{uri: nil, range: %{start: %{line: 1, column: 1}, end: %{line: 1, column: 4}}}, + %{uri: nil, range: %{start: %{line: 2, column: 1}, end: %{line: 2, column: 4}}} + ] + end + + test "find reference for variable split across lines", %{trace: trace} do + buffer = """ + defmodule MyModule do + def func do + var1 = + 1 + var1 + end + end + """ + + references = Locator.references(buffer, 3, 6, trace) + + assert references == [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 9}}}, + %{uri: nil, range: %{start: %{line: 5, column: 5}, end: %{line: 5, column: 9}}} + ] + end + + test "find references of variables in arguments", %{trace: trace} do + buffer = """ + defmodule MyModule do + def call(conn) do + if true do + conn + end + end + end + """ + + references = Locator.references(buffer, 2, 13, trace) + + assert references == [ + %{range: %{end: %{column: 16, line: 2}, start: %{column: 12, line: 2}}, uri: nil}, + %{range: %{end: %{column: 11, line: 4}, start: %{column: 7, line: 4}}, uri: nil} + ] + end + + test "find references for a redefined variable", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun(var) do + var = 1 + var + + var + end + end + """ + + # `var` defined in the function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 14}, end: %{line: 2, column: 17}}}, + %{uri: nil, range: %{start: %{line: 3, column: 15}, end: %{line: 3, column: 18}}} + ] + + assert Locator.references(buffer, 2, 14, trace) == expected_references + assert Locator.references(buffer, 3, 15, trace) == expected_references + + # `var` redefined in the function body + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 8}}}, + %{uri: nil, range: %{start: %{line: 5, column: 5}, end: %{line: 5, column: 8}}} + ] + + assert Locator.references(buffer, 3, 5, trace) == expected_references + assert Locator.references(buffer, 5, 5, trace) == expected_references + end + + test "find references for a variable in a guard", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun(var) when is_atom(var) do + case var do + var when var > 0 -> var + end + + Enum.map([1, 2], fn x when x > 0 -> x end) + end + end + """ + + # `var` defined in the function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 14}, end: %{line: 2, column: 17}}}, + %{uri: nil, range: %{start: %{line: 2, column: 32}, end: %{line: 2, column: 35}}}, + %{uri: nil, range: %{start: %{line: 3, column: 10}, end: %{line: 3, column: 13}}} + ] + + assert Locator.references(buffer, 2, 14, trace) == expected_references + assert Locator.references(buffer, 2, 32, trace) == expected_references + assert Locator.references(buffer, 3, 10, trace) == expected_references + + # `var` defined in the case clause + expected_references = [ + %{uri: nil, range: %{start: %{line: 4, column: 7}, end: %{line: 4, column: 10}}}, + %{uri: nil, range: %{start: %{line: 4, column: 16}, end: %{line: 4, column: 19}}}, + %{uri: nil, range: %{start: %{line: 4, column: 27}, end: %{line: 4, column: 30}}} + ] + + assert Locator.references(buffer, 4, 7, trace) == expected_references + assert Locator.references(buffer, 4, 16, trace) == expected_references + assert Locator.references(buffer, 4, 27, trace) == expected_references + + # `x` + expected_references = [ + %{uri: nil, range: %{start: %{line: 7, column: 25}, end: %{line: 7, column: 26}}}, + %{uri: nil, range: %{start: %{line: 7, column: 32}, end: %{line: 7, column: 33}}}, + %{uri: nil, range: %{start: %{line: 7, column: 41}, end: %{line: 7, column: 42}}} + ] + + assert Locator.references(buffer, 7, 25, trace) == expected_references + assert Locator.references(buffer, 7, 32, trace) == expected_references + assert Locator.references(buffer, 7, 41, trace) == expected_references + end + + test "find references for variable in inner scopes", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun([h | t]) do + sum = h + my_fun(t) + + if h > sum do + h + sum + else + h = my_fun(t) + sum + h + end + end + end + """ + + # `h` from the function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 15}, end: %{line: 2, column: 16}}}, + %{uri: nil, range: %{start: %{line: 3, column: 11}, end: %{line: 3, column: 12}}}, + %{uri: nil, range: %{start: %{line: 5, column: 8}, end: %{line: 5, column: 9}}}, + %{uri: nil, range: %{start: %{line: 6, column: 7}, end: %{line: 6, column: 8}}} + ] + + Enum.each([{2, 15}, {3, 11}, {5, 8}, {6, 7}], fn {line, column} -> + assert Locator.references(buffer, line, column, trace) == expected_references + end) + + # `h` from the if-else scope + expected_references = [ + %{uri: nil, range: %{start: %{line: 8, column: 7}, end: %{line: 8, column: 8}}}, + %{uri: nil, range: %{start: %{line: 9, column: 7}, end: %{line: 9, column: 8}}} + ] + + assert Locator.references(buffer, 8, 7, trace) == expected_references + assert Locator.references(buffer, 9, 7, trace) == expected_references + + # `sum` + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 8}}}, + %{uri: nil, range: %{start: %{line: 5, column: 12}, end: %{line: 5, column: 15}}}, + %{uri: nil, range: %{start: %{line: 6, column: 11}, end: %{line: 6, column: 14}}}, + %{uri: nil, range: %{start: %{line: 8, column: 23}, end: %{line: 8, column: 26}}} + ] + + Enum.each([{3, 5}, {5, 12}, {6, 11}, {8, 23}], fn {line, column} -> + assert Locator.references(buffer, line, column, trace) == expected_references + end) + end + + test "find references for variable from the scope of an anonymous function", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun(x, y) do + x = Enum.map(x, fn x -> x + y end) + end + end + """ + + # `x` from the `my_fun` function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 14}, end: %{line: 2, column: 15}}}, + %{uri: nil, range: %{start: %{line: 3, column: 18}, end: %{line: 3, column: 19}}} + ] + + assert Locator.references(buffer, 2, 14, trace) == expected_references + assert Locator.references(buffer, 3, 18, trace) == expected_references + + # `y` from the `my_fun` function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 17}, end: %{line: 2, column: 18}}}, + %{uri: nil, range: %{start: %{line: 3, column: 33}, end: %{line: 3, column: 34}}} + ] + + assert Locator.references(buffer, 2, 17, trace) == expected_references + assert Locator.references(buffer, 3, 33, trace) == expected_references + + # `x` from the anonymous function + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 24}, end: %{line: 3, column: 25}}}, + %{uri: nil, range: %{start: %{line: 3, column: 29}, end: %{line: 3, column: 30}}} + ] + + assert Locator.references(buffer, 3, 24, trace) == expected_references + assert Locator.references(buffer, 3, 29, trace) == expected_references + + # redefined `x` + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 6}}} + ] + + assert Locator.references(buffer, 3, 5, trace) == expected_references + end + + test "find references of a variable when using pin operator", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun(a, b) do + case a do + ^b -> b + %{b: ^b} = a -> b + end + end + end + """ + + # `b` + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 17}, end: %{line: 2, column: 18}}}, + %{uri: nil, range: %{start: %{line: 4, column: 8}, end: %{line: 4, column: 9}}}, + %{uri: nil, range: %{start: %{line: 4, column: 13}, end: %{line: 4, column: 14}}}, + %{uri: nil, range: %{start: %{line: 5, column: 13}, end: %{line: 5, column: 14}}}, + %{uri: nil, range: %{start: %{line: 5, column: 23}, end: %{line: 5, column: 24}}} + ] + + assert Locator.references(buffer, 2, 17, trace) == expected_references + assert Locator.references(buffer, 4, 8, trace) == expected_references + assert Locator.references(buffer, 4, 13, trace) == expected_references + assert Locator.references(buffer, 5, 13, trace) == expected_references + assert Locator.references(buffer, 5, 23, trace) == expected_references + + # `a` redefined in a case clause + expected_references = [ + %{uri: nil, range: %{start: %{line: 5, column: 18}, end: %{line: 5, column: 19}}} + ] + + assert Locator.references(buffer, 5, 18, trace) == expected_references + end + + test "find references of a variable in multiline struct", %{trace: trace} do + buffer = """ + defmodule MyServer do + def go do + %Some{ + filed: my_var, + other: some, + other: my_var + } = abc() + fun(my_var, some) + end + end + """ + + # `my_var` + expected_references = [ + %{uri: nil, range: %{start: %{line: 4, column: 14}, end: %{line: 4, column: 20}}}, + %{uri: nil, range: %{start: %{line: 6, column: 14}, end: %{line: 6, column: 20}}}, + %{uri: nil, range: %{start: %{line: 8, column: 9}, end: %{line: 8, column: 15}}} + ] + + assert Locator.references(buffer, 4, 15, trace) == expected_references + assert Locator.references(buffer, 6, 15, trace) == expected_references + assert Locator.references(buffer, 8, 10, trace) == expected_references + end + + test "find references of a variable shadowing function", %{trace: trace} do + buffer = """ + defmodule Vector do + @spec magnitude(Vec2.t()) :: number() + def magnitude(%Vec2{} = v), do: :math.sqrt(:math.pow(v.x, 2) + :math.pow(v.y, 2)) + + @spec normalize(Vec2.t()) :: Vec2.t() + def normalize(%Vec2{} = v) do + length = magnitude(v) + %{v | x: v.x / length, y: v.y / length} + end + end + """ + + # `my_var` + expected_references = [ + %{uri: nil, range: %{start: %{line: 7, column: 5}, end: %{line: 7, column: 11}}}, + %{uri: nil, range: %{start: %{line: 8, column: 20}, end: %{line: 8, column: 26}}}, + %{uri: nil, range: %{start: %{line: 8, column: 37}, end: %{line: 8, column: 43}}} + ] + + assert Locator.references(buffer, 7, 6, trace) == expected_references + assert Locator.references(buffer, 8, 21, trace) == expected_references + end + + test "find references of attributes", %{trace: trace} do + buffer = """ + defmodule MyModule do + @attr "abc" + def fun do + @attr + end + end + """ + + references = Locator.references(buffer, 4, 7, trace) + + assert references == [ + %{range: %{end: %{column: 8, line: 2}, start: %{column: 3, line: 2}}, uri: nil}, + %{range: %{end: %{column: 10, line: 4}, start: %{column: 5, line: 4}}, uri: nil} + ] + + references = Locator.references(buffer, 2, 4, trace) + + assert references == [ + %{range: %{end: %{column: 8, line: 2}, start: %{column: 3, line: 2}}, uri: nil}, + %{range: %{end: %{column: 10, line: 4}, start: %{column: 5, line: 4}}, uri: nil} + ] + end + + test "find references of private functions from definition", %{trace: trace} do + buffer = """ + defmodule MyModule do + def calls_private do + private_fun() + end + + defp also_calls_private do + private_fun() + end + + defp private_fun do + # ^ + :ok + end + end + """ + + references = Locator.references(buffer, 10, 15, trace) + + assert references == [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 16}}}, + %{uri: nil, range: %{start: %{line: 7, column: 5}, end: %{line: 7, column: 16}}} + ] + end + + test "find references of private functions from invocation", %{trace: trace} do + buffer = """ + defmodule MyModule do + def calls_private do + private_fun() + # ^ + end + + defp also_calls_private do + private_fun() + end + + defp private_fun do + :ok + end + end + """ + + references = Locator.references(buffer, 3, 15, trace) + + assert references == [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 16}}}, + %{uri: nil, range: %{start: %{line: 8, column: 5}, end: %{line: 8, column: 16}}} + ] + end + + test "find references of public metadata functions from definition", %{trace: trace} do + buffer = """ + defmodule MyModule do + def calls_public do + MyCalleeModule.Some.public_fun() + end + + defp also_calls_public do + alias MyCalleeModule.Some + Some.public_fun() + end + + defp also_calls_public_import do + import MyCalleeModule.Some + public_fun() + end + end + + defmodule MyCalleeModule.Some do + def public_fun do + # ^ + :ok + end + end + """ + + references = Locator.references(buffer, 18, 15, trace) + + assert references == [ + %{uri: nil, range: %{start: %{line: 3, column: 25}, end: %{line: 3, column: 35}}}, + %{uri: nil, range: %{start: %{line: 8, column: 10}, end: %{line: 8, column: 20}}}, + %{uri: nil, range: %{start: %{line: 13, column: 5}, end: %{line: 13, column: 15}}} + ] + end + + test "does not find references of private metadata functions from definition", %{trace: trace} do + buffer = """ + defmodule MyModule do + def calls_public do + MyCalleeModule.Some.public_fun() + end + + defp also_calls_public do + alias MyCalleeModule.Some + Some.public_fun() + end + + defp also_calls_public_import do + import MyCalleeModule.Some + public_fun() + end + end + + defmodule MyCalleeModule.Some do + defp public_fun do + # ^ + :ok + end + end + """ + + references = Locator.references(buffer, 18, 15, trace) + + assert references == [] + end + + test "find references with cursor over a module", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee1.func() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 53, trace) + + assert [ + %{range: %{end: %{column: 62, line: 3}, start: %{column: 58, line: 3}}, uri: nil}, + %{ + uri: "test/support/modules_with_references.ex", + range: range_1 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_2 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_3 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_4 + }, + %{ + uri: "test/support/modules_with_references.ex", + range: range_5 + } + ] = references + + assert range_1 == + %{start: %{line: 36, column: 60}, end: %{line: 36, column: 64}} |> maybe_shift + + assert range_2 == + %{start: %{line: 42, column: 60}, end: %{line: 42, column: 64}} |> maybe_shift + + assert range_3 == + %{start: %{line: 65, column: 16}, end: %{line: 65, column: 20}} |> maybe_shift + + assert range_4 == + %{start: %{line: 65, column: 63}, end: %{line: 65, column: 67}} |> maybe_shift + + assert range_5 == + %{start: %{line: 65, column: 79}, end: %{line: 65, column: 83}} |> maybe_shift + end + + test "find references with cursor over an erlang module", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + :ets.new(:s, []) + # ^ + end + end + """ + + references = + Locator.references(buffer, 3, 7, trace) + |> Enum.filter(&(&1.uri == nil or &1.uri =~ "modules_with_references")) + + assert [ + %{ + range: %{end: %{column: 13, line: 3}, start: %{column: 10, line: 3}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/modules_with_references.ex" + } + ] = references + + assert range_1 == + %{start: %{column: 12, line: 74}, end: %{column: 15, line: 74}} |> maybe_shift + end + + test "find references with cursor over an erlang function call", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + :ets.new(:s, []) + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 11, trace) + + assert [ + %{ + range: %{end: %{column: 13, line: 3}, start: %{column: 10, line: 3}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/modules_with_references.ex" + } + ] = references + + assert range_1 == + %{start: %{column: 12, line: 74}, end: %{column: 15, line: 74}} |> maybe_shift + end + + test "find references with cursor over builtin function call", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee6.module_info() + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 60, trace) + + assert [ + %{ + range: %{end: %{column: 69, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/modules_with_references.ex" + } + ] = references + + assert range_1 == + %{start: %{column: 60, line: 101}, end: %{column: 71, line: 101}} |> maybe_shift + end + + test "find references with cursor over builtin function call incomplete code", %{trace: trace} do + buffer = """ + defmodule Caller do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee6.module_info( + # ^ + end + end + """ + + references = Locator.references(buffer, 3, 60, trace) + + assert [ + %{ + range: %{end: %{column: 69, line: 3}, start: %{column: 58, line: 3}}, + uri: nil + }, + %{ + range: range_1, + uri: "test/support/modules_with_references.ex" + } + ] = references + + assert range_1 == + %{start: %{column: 60, line: 101}, end: %{column: 71, line: 101}} |> maybe_shift + end + + defp read_line(file, range) do + {line, column} = {range.start.line, range.start.column} + + file + |> File.read!() + |> Source.split_lines() + |> Enum.at(line - 1) + |> String.slice((column - 1)..-1//1) + end + + defp maybe_shift(%{ + start: %{column: column_start, line: line_start}, + end: %{column: column_end, line: line_end} + }) do + if Version.match?(System.version(), ">= 1.13.0") do + %{ + start: %{column: column_start, line: line_start}, + end: %{column: column_end, line: line_end} + } + else + %{ + start: %{column: column_start - 1, line: line_start}, + end: %{column: column_end - 1, line: line_end} + } + end + end +end diff --git a/apps/language_server/test/providers/signature_help/signature_test.exs b/apps/language_server/test/providers/signature_help/signature_test.exs new file mode 100644 index 000000000..70074e0c8 --- /dev/null +++ b/apps/language_server/test/providers/signature_help/signature_test.exs @@ -0,0 +1,1591 @@ +defmodule ElixirLS.LanguageServer.Providers.SignatureHelp.SignatureTest do + use ExUnit.Case, async: true + alias ElixirLS.LanguageServer.Providers.SignatureHelp.Signature + + describe "type signature" do + test "find signatures from local type" do + code = """ + defmodule MyModule do + @typep my(a) :: {a, nil} + @typep my(a, b) :: {a, b} + @type a :: my( + end + """ + + assert Signature.signature(code, 4, 19) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "my", + params: ["a"], + spec: "@typep my(a) :: {a, nil}" + }, + %{ + documentation: "", + name: "my", + params: ["a", "b"], + spec: "@typep my(a, b) :: {a, b}" + } + ] + } + end + + test "find signatures from local type, filter by arity" do + code = """ + defmodule MyModule do + @typep my(a) :: {a, nil} + @typep my(a, b) :: {a, b} + @type a :: my(atom, + end + """ + + assert Signature.signature(code, 4, 25) == %{ + active_param: 1, + signatures: [ + %{ + documentation: "", + name: "my", + params: ["a", "b"], + spec: "@typep my(a, b) :: {a, b}" + } + ] + } + end + + test "find signatures from local type, filter by arity unfinished param" do + code = """ + defmodule MyModule do + @typep my(a) :: {a, nil} + @typep my(a, b) :: {a, b} + @type a :: my(atom + end + """ + + assert Signature.signature(code, 4, 24) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "my", + params: ["a"], + spec: "@typep my(a) :: {a, nil}" + }, + %{ + documentation: "", + name: "my", + params: ["a", "b"], + spec: "@typep my(a, b) :: {a, b}" + } + ] + } + end + + test "find signatures from local type, filter by arity unfinished params" do + code = """ + defmodule MyModule do + @typep my(a) :: {a, nil} + @typep my(a, b) :: {a, b} + @type a :: my(atom, atom + end + """ + + assert Signature.signature(code, 4, 30) == %{ + active_param: 1, + signatures: [ + %{ + documentation: "", + name: "my", + params: ["a", "b"], + spec: "@typep my(a, b) :: {a, b}" + } + ] + } + end + + test "find local metadata type signature even if it's defined after cursor" do + buffer = """ + defmodule MyModule do + @type remote_list_t :: [my_t(a)] + # ^ + + @typep my_t(abc) :: integer + end + """ + + assert %{ + active_param: 0 + } = + Signature.signature(buffer, 2, 32) + end + + test "find type signatures" do + code = """ + defmodule MyModule do + @type a :: ElixirSenseExample.ModuleWithTypespecs.Remote.remote_t( + end + """ + + assert Signature.signature(code, 2, 69) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "Remote type", + name: "remote_t", + params: [], + spec: "@type remote_t() :: atom()" + }, + %{ + documentation: "Remote type with params", + name: "remote_t", + params: ["a", "b"], + spec: "@type remote_t(a, b) ::\n {a, b}" + } + ] + } + end + + test "does not reveal opaque type details" do + code = """ + defmodule MyModule do + @type a :: ElixirSenseExample.ModuleWithTypespecs.Remote.some_opaque_options_t( + end + """ + + assert Signature.signature(code, 2, 82) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "some_opaque_options_t", + params: [], + spec: "@opaque some_opaque_options_t()" + } + ] + } + end + + test "does not reveal local opaque type details" do + code = """ + defmodule Some do + @opaque my(a, b) :: {a, b} + end + defmodule MyModule do + @type a :: Some.my( + end + """ + + assert Signature.signature(code, 5, 22) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "my", + params: ["a", "b"], + spec: "@opaque my(a, b)" + } + ] + } + end + + test "find type signatures with @typedoc false" do + code = """ + defmodule MyModule do + @type a :: ElixirSenseExample.ModuleWithDocs.some_type_doc_false( + end + """ + + assert Signature.signature(code, 2, 68) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "some_type_doc_false", + params: ~c"", + spec: "@type some_type_doc_false() :: integer()" + } + ] + } + end + + test "does not find builtin type signatures with Elixir prefix" do + code = """ + defmodule MyModule do + @type a :: Elixir.keyword( + end + """ + + assert Signature.signature(code, 2, 29) == :none + end + + test "find type signatures from erlang module" do + code = """ + defmodule MyModule do + @type a :: :erlang.time_unit( + end + """ + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: summary, + name: "time_unit", + params: [], + spec: "@type time_unit() ::" <> _ + } + ] + } = Signature.signature(code, 2, 32) + + if System.otp_release() |> String.to_integer() >= 23 do + assert summary =~ "Supported time unit representations:" + end + end + + test "find type signatures from builtin type" do + code = """ + defmodule MyModule do + @type a :: number( + end + """ + + assert Signature.signature(code, 2, 21) == %{ + active_param: 0, + signatures: [ + %{ + params: [], + documentation: "An integer or a float", + name: "number", + spec: "@type number() :: integer() | float()" + } + ] + } + end + end + + describe "macro signature" do + test "find signatures from aliased modules" do + code = """ + defmodule MyModule do + require ElixirSenseExample.BehaviourWithMacrocallback.Impl, as: Macros + Macros.some( + end + """ + + assert Signature.signature(code, 3, 15) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "some macro\n", + name: "some", + params: ["var"], + spec: + "@spec some(integer()) :: Macro.t()\n@spec some(b) :: Macro.t() when b: float()" + } + ] + } + end + + test "find signatures special forms" do + code = """ + defmodule MyModule do + __MODULE__( + end + """ + + assert Signature.signature(code, 2, 14) == %{ + active_param: 0, + signatures: [ + %{ + documentation: + "Returns the current module name as an atom or `nil` otherwise.", + name: "__MODULE__", + params: [], + spec: "" + } + ] + } + end + end + + describe "function signature" do + test "find signatures from erlang module" do + code = """ + defmodule MyModule do + :lists.flatten( + end + """ + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: summary1, + name: "flatten", + params: ["deepList"], + spec: + "@spec flatten(deepList) :: list when deepList: [term() | deepList], list: [term()]" + }, + %{ + documentation: summary2, + name: "flatten", + params: ["deepList", "tail"], + spec: + "@spec flatten(deepList, tail) :: list when deepList: [term() | deepList], tail: [term()], list: [term()]" + } + ] + } = Signature.signature(code, 2, 24) + + if System.otp_release() |> String.to_integer() >= 23 do + assert "Returns a flattened version of `DeepList`\\." <> _ = summary1 + + assert "Returns a flattened version of `DeepList` with tail `Tail` appended\\." <> _ = + summary2 + end + end + + test "find signatures from aliased modules" do + code = """ + defmodule MyModule do + alias List, as: MyList + MyList.flatten( + end + """ + + assert Signature.signature(code, 3, 23) == %{ + active_param: 0, + signatures: [ + %{ + name: "flatten", + params: ["list"], + documentation: "Flattens the given `list` of nested lists.", + spec: "@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]" + }, + %{ + name: "flatten", + params: ["list", "tail"], + documentation: + "Flattens the given `list` of nested lists.\nThe list `tail` will be added at the end of\nthe flattened list.", + spec: + "@spec flatten(deep_list, [elem]) :: [elem] when deep_list: [elem | deep_list], elem: var" + } + ] + } + end + + test "find signatures from aliased modules aaa" do + code = """ + defmodule MyModule do + alias NonExisting, as: List + Elixir.List.flatten( + end + """ + + assert Signature.signature(code, 3, 28) == %{ + active_param: 0, + signatures: [ + %{ + name: "flatten", + params: ["list"], + documentation: "Flattens the given `list` of nested lists.", + spec: "@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]" + }, + %{ + name: "flatten", + params: ["list", "tail"], + documentation: + "Flattens the given `list` of nested lists.\nThe list `tail` will be added at the end of\nthe flattened list.", + spec: + "@spec flatten(deep_list, [elem]) :: [elem] when deep_list: [elem | deep_list], elem: var" + } + ] + } + end + + test "find signatures from imported modules" do + code = """ + defmodule MyModule do + import List + flatten( + end + """ + + assert Signature.signature(code, 3, 16) == %{ + active_param: 0, + signatures: [ + %{ + name: "flatten", + params: ["list"], + documentation: "Flattens the given `list` of nested lists.", + spec: "@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]" + }, + %{ + name: "flatten", + params: ["list", "tail"], + documentation: + "Flattens the given `list` of nested lists.\nThe list `tail` will be added at the end of\nthe flattened list.", + spec: + "@spec flatten(deep_list, [elem]) :: [elem] when deep_list: [elem | deep_list], elem: var" + } + ] + } + end + + test "find signatures when function with default args" do + code = """ + defmodule MyModule do + List.pop_at(par1, + end + """ + + assert Signature.signature(code, 2, 21) == %{ + active_param: 1, + signatures: [ + %{ + documentation: + "Returns and removes the value at the specified `index` in the `list`.", + name: "pop_at", + params: ["list", "index", "default \\\\ nil"], + spec: "@spec pop_at(list(), integer(), any()) :: {any(), list()}" + } + ] + } + end + + test "find signatures when function with many clauses" do + code = """ + defmodule MyModule do + List.starts_with?( + end + """ + + assert Signature.signature(code, 2, 21) == %{ + active_param: 0, + signatures: [ + %{ + documentation: + "Returns `true` if `list` starts with the given `prefix` list; otherwise returns `false`.", + name: "starts_with?", + params: ["list", "prefix"], + spec: + "@spec starts_with?([...], [...]) :: boolean()\n@spec starts_with?(list(), []) :: true\n@spec starts_with?([], [...]) :: false" + } + ] + } + end + + test "find signatures for function with @doc false" do + code = """ + defmodule MyModule do + ElixirSenseExample.ModuleWithDocs.some_fun_doc_false( + end + """ + + assert Signature.signature(code, 2, 56) == %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "some_fun_doc_false", + params: ["a", "b \\\\ nil"], + spec: "" + } + ] + } + end + + test "find signatures from atom modules" do + code = """ + defmodule MyModule do + :"Elixir.List".flatten( + end + """ + + assert Signature.signature(code, 2, 31) == %{ + active_param: 0, + signatures: [ + %{ + name: "flatten", + params: ["list"], + documentation: "Flattens the given `list` of nested lists.", + spec: "@spec flatten(deep_list) :: list() when deep_list: [any() | deep_list]" + }, + %{ + name: "flatten", + params: ["list", "tail"], + documentation: + "Flattens the given `list` of nested lists.\nThe list `tail` will be added at the end of\nthe flattened list.", + spec: + "@spec flatten(deep_list, [elem]) :: [elem] when deep_list: [elem | deep_list], elem: var" + } + ] + } + end + + test "find signatures from __MODULE__" do + code = """ + defmodule Inspect.Algebra do + __MODULE__.glue(par1, + end + """ + + assert Signature.signature(code, 2, 24) == %{ + active_param: 1, + signatures: [ + %{ + documentation: + "Glues two documents (`doc1` and `doc2`) inserting the given\nbreak `break_string` between them.", + name: "glue", + params: ["doc1", "break_string \\\\ \" \"", "doc2"], + spec: "@spec glue(t(), binary(), t()) :: t()", + active_param: 2 + } + ] + } + end + + test "find signatures from __MODULE__ submodule" do + code = """ + defmodule Inspect do + __MODULE__.Algebra.glue(par1, + end + """ + + assert Signature.signature(code, 2, 32) == %{ + active_param: 1, + signatures: [ + %{ + documentation: + "Glues two documents (`doc1` and `doc2`) inserting the given\nbreak `break_string` between them.", + name: "glue", + params: ["doc1", "break_string \\\\ \" \"", "doc2"], + spec: "@spec glue(t(), binary(), t()) :: t()", + active_param: 2 + } + ] + } + end + + test "find signatures from attribute" do + code = """ + defmodule MyMod do + @attribute Inspect.Algebra + @attribute.glue(par1, + end + """ + + assert Signature.signature(code, 3, 24) == %{ + active_param: 1, + signatures: [ + %{ + documentation: + "Glues two documents (`doc1` and `doc2`) inserting the given\nbreak `break_string` between them.", + name: "glue", + params: ["doc1", "break_string \\\\ \" \"", "doc2"], + spec: "@spec glue(t(), binary(), t()) :: t()", + active_param: 2 + } + ] + } + end + + @tag :capture_log + test "find signatures from attribute submodule" do + code = """ + defmodule Inspect do + @attribute Inspect + @attribute.Algebra.glue(par1, + end + """ + + assert Signature.signature(code, 3, 32) == %{ + active_param: 1, + signatures: [ + %{ + documentation: + "Glues two documents (`doc1` and `doc2`) inserting the given\nbreak `break_string` between them.", + name: "glue", + params: ["doc1", "break_string \\\\ \" \"", "doc2"], + spec: "@spec glue(t(), binary(), t()) :: t()", + active_param: 2 + } + ] + } + end + + test "find signatures from variable" do + code = """ + defmodule MyMod do + myvariable = Inspect.Algebra + myvariable.glue(par1, + end + """ + + assert Signature.signature(code, 3, 24) == %{ + active_param: 1, + signatures: [ + %{ + documentation: + "Glues two documents (`doc1` and `doc2`) inserting the given\nbreak `break_string` between them.", + name: "glue", + params: ["doc1", "break_string \\\\ \" \"", "doc2"], + spec: "@spec glue(t(), binary(), t()) :: t()", + active_param: 2 + } + ] + } + end + + @tag :capture_log + test "find signatures from variable submodule - don't crash" do + code = """ + defmodule Inspect do + myvariable = Inspect + myvariable.Algebra.glue(par1, + end + """ + + assert Signature.signature(code, 3, 32) == :none + end + + test "find signatures from variable call" do + code = """ + defmodule Inspect do + myvariable = &Inspect.Algebra.glue/2 + myvariable.(par1, + end + """ + + # TODO https://github.com/elixir-lsp/elixir_sense/issues/255 + # Type system needs to handle function captures + assert Signature.signature(code, 3, 20) == :none + end + + test "find signatures from attribute call" do + code = """ + defmodule Inspect do + @attribute &Inspect.Algebra.glue/2 + @attribute.(par1, + end + """ + + # TODO https://github.com/elixir-lsp/elixir_sense/issues/255 + # Type system needs to handle function captures + assert Signature.signature(code, 3, 20) == :none + end + + test "finds signatures from Kernel functions" do + code = """ + defmodule MyModule do + apply(par1, + end + """ + + assert Signature.signature(code, 2, 14) == %{ + active_param: 1, + signatures: [ + %{ + name: "apply", + params: ["fun", "args"], + documentation: + "Invokes the given anonymous function `fun` with the list of\narguments `args`.", + spec: "@spec apply((... -> any()), [any()]) :: any()" + }, + %{ + name: "apply", + params: ["module", "function_name", "args"], + documentation: + "Invokes the given function from `module` with the list of\narguments `args`.", + spec: "@spec apply(module(), function_name :: atom(), [any()]) :: any()" + } + ] + } + end + + test "finds signatures from local functions" do + code = """ + defmodule MyModule do + + def run do + sum( + end + + defp sum(a, b) do + a + b + end + + defp sum({a, b}) do + a + b + end + end + """ + + assert Signature.signature(code, 4, 9) == %{ + active_param: 0, + signatures: [ + %{ + name: "sum", + params: ["tuple"], + documentation: "", + spec: "" + }, + %{ + name: "sum", + params: ["a", "b"], + documentation: "", + spec: "" + } + ] + } + end + + test "finds signatures from local functions, filter by arity" do + code = """ + defmodule MyModule do + + def run do + sum(a, + end + + defp sum(a, b) do + a + b + end + + defp sum({a, b}) do + a + b + end + end + """ + + assert Signature.signature(code, 4, 12) == %{ + active_param: 1, + signatures: [ + %{ + name: "sum", + params: ["a", "b"], + documentation: "", + spec: "" + } + ] + } + end + + test "finds signatures from module with many function clauses" do + code = """ + defmodule Other do + alias ElixirSenseExample.ModuleWithManyClauses, as: MyModule + def run do + MyModule.sum(a, + end + end + """ + + assert Signature.signature(code, 4, 21) == %{ + active_param: 1, + signatures: [ + %{ + documentation: "", + name: "sum", + spec: "", + params: ["s \\\\ nil", "f"], + active_param: 0 + }, + %{documentation: "", name: "sum", spec: "", params: ["arg", "x", "y"]} + ] + } + end + + test "finds signatures from metadata module functions" do + code = """ + defmodule MyModule do + def sum(s \\\\ nil, f) + def sum(a, nil), do: nil + def sum(a, b) do + a + b + end + + def sum({a, b}, x, y) do + a + b + x + y + end + end + + defmodule Other do + def run do + MyModule.sum(a, + end + end + """ + + assert Signature.signature(code, 15, 21) == %{ + active_param: 1, + signatures: [ + %{ + documentation: "", + name: "sum", + params: ["s \\\\ nil", "f"], + spec: "", + active_param: 0 + }, + %{documentation: "", name: "sum", params: ["tuple", "x", "y"], spec: ""} + ] + } + end + + test "does not finds signatures from metadata module private functions" do + code = """ + defmodule MyModule do + defp sum(a, nil), do: nil + defp sum(a, b) do + a + b + end + + defp sum({a, b}) do + a + b + end + end + + defmodule Other do + def run do + MyModule.sum(a, + end + end + """ + + assert Signature.signature(code, 14, 21) == :none + end + + test "finds signatures from metadata module functions with default param" do + code = """ + defmodule MyModule do + @spec sum(integer, integer) :: integer + defp sum(a, b \\\\ 0) do + a + b + end + + def run do + sum(a, + end + end + """ + + assert Signature.signature(code, 8, 11) == %{ + active_param: 1, + signatures: [ + %{ + name: "sum", + params: ["a", "b \\\\ 0"], + documentation: "", + spec: "@spec sum(integer, integer) :: integer" + } + ] + } + end + + test "finds signatures from metadata module functions with default param - correctly highlight active param" do + code = """ + defmodule MyModule do + @spec sum(integer, integer, integer, integer, integer, integer) :: integer + defp sum(a \\\\ 1, b \\\\ 1, c, d, e \\\\ 1, f \\\\ 1) do + a + b + end + + def run do + sum(1, 2, 3, 4, 5, 6) + end + end + """ + + assert Signature.signature(code, 8, 10) == %{ + active_param: 0, + signatures: [ + %{ + name: "sum", + params: [ + "a \\\\ 1", + "b \\\\ 1", + "c", + "d", + "e \\\\ 1", + "f \\\\ 1" + ], + documentation: "", + spec: + "@spec sum(integer, integer, integer, integer, integer, integer) :: integer", + active_param: 2 + } + ] + } + + assert %{ + active_param: 1, + signatures: [%{active_param: 3}] + } = Signature.signature(code, 8, 13) + + assert %{ + active_param: 2, + signatures: [%{active_param: 0}] + } = Signature.signature(code, 8, 16) + + assert %{ + active_param: 3, + signatures: [%{active_param: 1}] + } = Signature.signature(code, 8, 19) + + assert %{ + active_param: 4, + signatures: [signature] + } = Signature.signature(code, 8, 22) + + refute Map.has_key?(signature, :active_param) + + assert %{ + active_param: 5, + signatures: [signature] + } = Signature.signature(code, 8, 25) + + refute Map.has_key?(signature, :active_param) + end + + test "finds signatures from metadata elixir behaviour call" do + code = """ + defmodule MyModule do + use GenServer + + def handle_call(request, _from, state) do + terminate() + end + + def init(arg), do: arg + + def handle_cast(arg, _state) when is_atom(arg) do + :ok + end + end + """ + + assert %{ + active_param: 0, + signatures: [ + %{ + name: "terminate", + params: ["_reason", "_state"], + documentation: "Invoked when the server is about to exit" <> _, + spec: "@callback terminate(reason, state :: term()) :: term()" <> _ + } + ] + } = Signature.signature(code, 5, 15) + end + + test "finds signatures from metadata erlang behaviour call" do + code = """ + defmodule MyModule do + @behaviour :gen_server + + def handle_call(request, _from, state) do + init() + end + + def init(arg), do: arg + + def handle_cast(arg, _state) when is_atom(arg) do + :ok + end + end + """ + + assert %{ + active_param: 0, + signatures: [ + %{ + name: "init", + params: ["arg"], + documentation: summary, + spec: "@callback init(args :: term()) ::" <> _ + } + ] + } = Signature.signature(code, 5, 10) + + if System.otp_release() |> String.to_integer() >= 23 do + assert "- Args = term\\(\\)\n- Result" <> _ = summary + end + end + + test "finds signatures from metadata elixir behaviour call from outside" do + code = """ + require ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl + ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl.bar() + """ + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "Docs for bar", + name: "bar", + params: ["b"], + spec: "@macrocallback bar(integer()) :: Macro.t()" + } + ] + } = Signature.signature(code, 2, 60) + end + + test "finds signatures from metadata erlang behaviour implemented in elixir call from outside" do + code = """ + ElixirSenseExample.ExampleBehaviourWithDocCallbackErlang.init() + """ + + res = Signature.signature(code, 1, 63) + + if System.otp_release() |> String.to_integer() >= 23 do + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "- Args = " <> _, + name: "init", + params: ["_"], + spec: "@callback init(args :: term()) :: init_result(state())" + } + ] + } = res + end + end + + if System.otp_release() |> String.to_integer() >= 25 do + test "finds signatures from metadata erlang behaviour call from outside" do + code = """ + :file_server.init() + """ + + res = Signature.signature(code, 1, 19) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "- Args = " <> _, + name: "init", + params: ["args"], + spec: "@callback init(args :: term()) ::" <> _ + } + ] + } = res + end + end + + test "retrieve metadata function signature - fallback to callback in metadata" do + code = """ + defmodule MyBehaviour do + @doc "Sample doc" + @doc since: "1.2.3" + @callback flatten(list()) :: list() + end + + defmodule MyLocalModule do + @behaviour MyBehaviour + + @impl true + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + res = Signature.signature(code, 18, 27) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "Sample doc", + name: "flatten", + params: ["list"], + spec: "@callback flatten(list()) :: list()" + } + ] + } = res + end + + test "retrieve metadata function signature - fallback to protocol function in metadata" do + code = """ + defprotocol BB do + @doc "asdf" + @spec go(t) :: integer() + def go(t) + end + + defimpl BB, for: String do + def go(t), do: "" + end + + defmodule MyModule do + def func(list) do + BB.String.go(list) + end + end + """ + + res = Signature.signature(code, 13, 18) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "asdf", + name: "go", + params: ["t"], + spec: "@callback go(t) :: integer()" + } + ] + } = res + end + + test "retrieve metadata macro signature - fallback to macrocallback in metadata" do + code = """ + defmodule MyBehaviour do + @doc "Sample doc" + @doc since: "1.2.3" + @macrocallback flatten(list()) :: list() + end + + defmodule MyLocalModule do + @behaviour MyBehaviour + + @impl true + defmacro flatten(list) do + [] + end + end + + defmodule MyModule do + require MyLocalModule + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + res = Signature.signature(code, 19, 27) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "Sample doc", + name: "flatten", + params: ["list"], + spec: "@macrocallback flatten(list()) :: list()" + } + ] + } = res + end + + test "retrieve metadata function signature - fallback to callback" do + code = """ + defmodule MyLocalModule do + @behaviour ElixirSenseExample.BehaviourWithMeta + + @impl true + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + res = Signature.signature(code, 12, 27) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "Sample doc", + name: "flatten", + params: ["list"], + spec: "@callback flatten(list()) :: list()" + } + ] + } = res + end + + test "retrieve metadata function signature - fallback to erlang callback" do + code = """ + defmodule MyLocalModule do + @behaviour :gen_statem + + @impl true + def init(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.init(list) + end + end + """ + + res = Signature.signature(code, 12, 27) + + if System.otp_release() |> String.to_integer() >= 23 do + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "- Args = term" <> _, + name: "init", + params: ["list"], + spec: "@callback init(args :: term()) :: init_result(state())" + } + ] + } = res + end + end + + test "retrieve metadata macro signature - fallback to macrocallback" do + code = """ + defmodule MyLocalModule do + @behaviour ElixirSenseExample.BehaviourWithMeta + + @impl true + defmacro bar(list) do + [] + end + end + + defmodule MyModule do + require MyLocalModule + def func(list) do + MyLocalModule.bar(list) + end + end + """ + + res = Signature.signature(code, 13, 27) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "Docs for bar", + name: "bar", + params: ["list"], + spec: "@macrocallback bar(integer()) :: Macro.t()" + } + ] + } = res + end + + test "find signature of local macro" do + code = """ + defmodule MyModule do + defmacrop some(var), do: Macro.expand(var, __CALLER__) + + defmacro other do + some(1) + end + end + """ + + res = Signature.signature(code, 5, 10) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "some", + params: ["var"], + spec: "" + } + ] + } = res + end + + test "does not find signature of local macro if it's defined after the cursor" do + code = """ + defmodule MyModule do + defmacro other do + some(1) + end + + defmacrop some(var), do: Macro.expand(var, __CALLER__) + end + """ + + assert Signature.signature(code, 3, 10) == :none + end + + test "find signature of local function even if it's defined after the cursor" do + code = """ + defmodule MyModule do + def other do + some(1) + end + + defp some(var), do: :ok + end + """ + + assert res = Signature.signature(code, 3, 10) + + assert res == %{ + active_param: 0, + signatures: [%{documentation: "", name: "some", params: ["var"], spec: ""}] + } + end + + test "returns :none when it cannot identify a function call" do + code = """ + defmodule MyModule do + fn(a, + end + """ + + assert Signature.signature(code, 2, 8) == :none + end + + test "return :none when no signature is found" do + code = """ + defmodule MyModule do + a_func( + end + """ + + assert Signature.signature(code, 2, 10) == :none + end + + test "after |>" do + code = """ + defmodule MyModule do + {1, 2} |> IO.inspect( + end + """ + + assert %{ + active_param: 1, + signatures: [ + %{ + name: "inspect", + params: ["item", "opts \\\\ []"], + documentation: "Inspects and writes the given `item` to the device.", + spec: "@spec inspect(" <> _ + }, + %{ + name: "inspect", + params: ["device", "item", "opts"], + documentation: + "Inspects `item` according to the given options using the IO `device`.", + spec: "@spec inspect(device(), item, keyword()) :: item when item: var" + } + ] + } = Signature.signature(code, 2, 24) + end + + test "after |> variable" do + code = """ + s |> String.replace_prefix( + """ + + assert %{ + active_param: 1 + } = Signature.signature(code, 1, 28) + end + + test "find built-in functions" do + # module_info is defined by default for every elixir and erlang module + # __info__ is defined for every elixir module + # behaviour_info is defined for every behaviour and every protocol + buffer = """ + defmodule MyModule do + ElixirSenseExample.ModuleWithFunctions.module_info() + # ^ + ElixirSenseExample.ModuleWithFunctions.__info__(:macros) + # ^ + ElixirSenseExample.ExampleBehaviour.behaviour_info(:callbacks) + # ^ + end + """ + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "The `module_info/0` function" <> _, + name: "module_info", + params: [], + spec: + "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]" + }, + %{ + documentation: "The call `module_info(Key)`" <> _, + name: "module_info", + params: ["key"], + spec: """ + @spec module_info(:module) :: atom + @spec module_info(:attributes | :compile) :: [{atom, term}] + @spec module_info(:md5) :: binary + @spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}] + @spec module_info(:native) :: boolean\ + """ + } + ] + } = Signature.signature(buffer, 2, 54) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "Provides runtime informatio" <> _, + name: "__info__", + params: ["atom"], + spec: """ + @spec __info__(:attributes) :: keyword() + @spec __info__(:compile) :: [term()] + @spec __info__(:functions) :: [{atom, non_neg_integer}] + @spec __info__(:macros) :: [{atom, non_neg_integer}] + @spec __info__(:md5) :: binary() + @spec __info__(:module) :: module()\ + """ + } + ] + } = Signature.signature(buffer, 4, 51) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "The `behaviour_info(Key)`" <> _, + name: "behaviour_info", + params: ["key"], + spec: + "@spec behaviour_info(:callbacks | :optional_callbacks) :: [{atom, non_neg_integer}]" + } + ] + } = Signature.signature(buffer, 6, 54) + end + + test "built-in functions cannot be called locally" do + # module_info is defined by default for every elixir and erlang module + # __info__ is defined for every elixir module + # behaviour_info is defined for every behaviour and every protocol + buffer = """ + defmodule MyModule do + import GenServer + @ callback cb() :: term + module_info() + # ^ + __info__(:macros) + # ^ + behaviour_info(:callbacks) + # ^ + end + """ + + assert :none = Signature.signature(buffer, 4, 15) + + assert :none = Signature.signature(buffer, 6, 12) + + assert :none = Signature.signature(buffer, 8, 18) + end + + if System.otp_release() |> String.to_integer() >= 23 do + test "find built-in erlang functions" do + buffer = """ + defmodule MyModule do + :erlang.orelse() + # ^ + :erlang.or() + # ^ + end + """ + + %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "orelse", + params: ["term", "term"], + spec: "" + } + ] + } = Signature.signature(buffer, 2, 18) + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: "", + name: "or", + params: [_, _], + spec: "@spec boolean() or boolean() :: boolean()" + } + ] + } = Signature.signature(buffer, 4, 14) + end + end + + test "find :erlang module functions with different forms of typespecs" do + buffer = """ + defmodule MyModule do + :erlang.date() + # ^ + :erlang.cancel_timer() + # ^ + end + """ + + %{ + active_param: 0, + signatures: [ + %{ + documentation: summary, + name: "date", + params: [], + spec: "@spec date() :: date when date: :calendar.date()" + } + ] + } = Signature.signature(buffer, 2, 16) + + if System.otp_release() |> String.to_integer() >= 23 do + assert "Returns the current date as" <> _ = summary + end + + assert %{ + active_param: 0, + signatures: [ + %{ + documentation: summary1, + name: "cancel_timer", + params: ["timerRef"], + spec: "@spec cancel_timer(timerRef) :: result" <> _ + }, + %{ + documentation: summary2, + name: "cancel_timer", + params: ["timerRef", "options"], + spec: "@spec cancel_timer(timerRef, options) :: result" <> _ + } + ] + } = Signature.signature(buffer, 4, 24) + + if System.otp_release() |> String.to_integer() >= 23 do + assert "Cancels a timer\\." <> _ = summary1 + assert "Cancels a timer that has been created by" <> _ = summary2 + end + end + end +end diff --git a/apps/language_server/test/support/behaviour_implementations.ex b/apps/language_server/test/support/behaviour_implementations.ex new file mode 100644 index 000000000..7e7c51294 --- /dev/null +++ b/apps/language_server/test/support/behaviour_implementations.ex @@ -0,0 +1,8 @@ +defmodule ElixirSenseExample.DummyBehaviour do + @callback foo() :: any +end + +defmodule ElixirSenseExample.DummyBehaviourImplementation do + @behaviour ElixirSenseExample.DummyBehaviour + def foo(), do: :ok +end diff --git a/apps/language_server/test/support/behaviour_with_macrocallbacks.ex b/apps/language_server/test/support/behaviour_with_macrocallbacks.ex new file mode 100644 index 000000000..da86d0528 --- /dev/null +++ b/apps/language_server/test/support/behaviour_with_macrocallbacks.ex @@ -0,0 +1,33 @@ +defmodule ElixirSenseExample.BehaviourWithMacrocallback do + @doc """ + A required macrocallback + """ + @macrocallback required(atom) :: Macro.t() + + @doc """ + An optional macrocallback + """ + @macrocallback optional(a) :: Macro.t() when a: atom + + @optional_callbacks [optional: 1] +end + +defmodule ElixirSenseExample.BehaviourWithMacrocallback.Impl do + @behaviour ElixirSenseExample.BehaviourWithMacrocallback + defmacro required(var), do: Macro.expand(var, __CALLER__) + defmacro optional(var), do: Macro.expand(var, __CALLER__) + + @doc """ + some macro + """ + @spec some(integer) :: Macro.t() + @spec some(b) :: Macro.t() when b: float + + defmacro some(var), do: Macro.expand(var, __CALLER__) + + @doc """ + some macro with default arg + """ + @spec with_default(atom, list, integer) :: Macro.t() + defmacro with_default(a \\ :asdf, b, var \\ 0), do: Macro.expand({a, b, var}, __CALLER__) +end diff --git a/apps/language_server/test/support/callback_opaque.ex b/apps/language_server/test/support/callback_opaque.ex new file mode 100644 index 000000000..1bbd8db55 --- /dev/null +++ b/apps/language_server/test/support/callback_opaque.ex @@ -0,0 +1,15 @@ +defmodule ElixirSenseExample.CallbackOpaque do + @moduledoc """ + Behaviour with opaque type in callback + """ + + @typedoc """ + Opaque type + """ + @opaque t(x) :: {term, x} + + @doc """ + Does stuff to opaque arg + """ + @callback do_stuff(t(a), term) :: t(a) when a: any +end diff --git a/apps/language_server/test/support/case_template_example.ex b/apps/language_server/test/support/case_template_example.ex new file mode 100644 index 000000000..b98e6b404 --- /dev/null +++ b/apps/language_server/test/support/case_template_example.ex @@ -0,0 +1,9 @@ +defmodule ElixirSenseExample.CaseTemplateExample do + use ExUnit.CaseTemplate + + using do + quote do + alias Some.Module + end + end +end diff --git a/apps/language_server/test/support/empty_module.ex b/apps/language_server/test/support/empty_module.ex new file mode 100644 index 000000000..e6e743f2a --- /dev/null +++ b/apps/language_server/test/support/empty_module.ex @@ -0,0 +1,7 @@ +defmodule ElixirSenseExample.EmptyModule do + @moduledoc """ + Empty module without other functions + + More moduledoc + """ +end diff --git a/apps/language_server/test/support/example_behaviour.ex b/apps/language_server/test/support/example_behaviour.ex new file mode 100644 index 000000000..87c836a8a --- /dev/null +++ b/apps/language_server/test/support/example_behaviour.ex @@ -0,0 +1,311 @@ +defmodule MyMacros do + defmodule Nested do + end + + defmodule One do + end + + defmodule Two.Three do + end +end + +defmodule MyImports do + defmodule NestedImports do + end + + defmodule OneImports do + end + + defmodule Two.ThreeImports do + end +end + +defmodule UseWithCallbacks do + defmacro __before_compile__(_env) do + quote do: :ok + end +end + +defmodule ElixirSenseExample.Delegates do + def delegated_func(_a), do: :ok +end + +defprotocol ProtocolOutside do + def reverse(term) +end + +defmodule ElixirSenseExample.ExampleBehaviour do + @moduledoc """ + Example of a module that has a __using__ that defines callbacks. Patterned directly off of GenServer from Elixir 1.8.0 + """ + + @type name :: any + + @typedoc "The server reference" + @type server :: pid | name | {atom, node} + + @typedoc """ + Tuple describing the client of a call request. + `pid` is the PID of the caller and `tag` is a unique term used to identify the + call. + """ + @type from :: {pid, tag :: term} + + @callback handle_call(request :: term, from, state :: term) :: + {:reply, reply, new_state} + | {:reply, reply, new_state, timeout | :hibernate | {:continue, term}} + | {:noreply, new_state} + | {:noreply, new_state, timeout | :hibernate | {:continue, term}} + | {:stop, reason, reply, new_state} + | {:stop, reason, new_state} + when reply: term, new_state: term, reason: term + + alias ElixirSenseExample.ExampleBehaviour + + defmacro __using__(opts) do + quote location: :keep, bind_quoted: [opts: opts] do + @behaviour ExampleBehaviour + + if Module.get_attribute(__MODULE__, :doc) == nil do + @doc """ + Returns a specification to start this module under a supervisor. + See `Supervisor`. + """ + end + + # TODO: Remove this on Elixir v2.0 + @before_compile UseWithCallbacks + + @doc false + def handle_call(msg, _from, state) do + proc = + case Process.info(self(), :registered_name) do + {_, []} -> self() + {_, name} -> name + end + + # We do this to trick Dialyzer to not complain about non-local returns. + case :erlang.phash2(1, 1) do + 0 -> + raise "attempted to call ExampleBehaviour #{inspect(proc)} but no handle_call/3 clause was provided" + + 1 -> + {:stop, {:bad_call, msg}, state} + end + end + + defoverridable handle_call: 3 + + alias MyModule.Some.Nested, as: Utils + alias MyModule.Other.Nested + alias :ets, as: Ets + alias MyModule.{One, Two.Three} + alias MyModule.{Four} + # alias :lists + alias Elixir.Three.OutsideOfMyModule + require MyMacros + require MyMacros.Nested, as: NestedMacros + require :ets, as: ErlangMacros + require MyMacros.{One, Two.Three} + import Some.{List}, only: [] + import MyImports + import MyImports.NestedImports + import MyImports.{OneImports, Two.ThreeImports} + import :lists, only: [] + @my_attribute "my_attr" + @spec private_func() :: String.t() + defp private_func, do: @my_attribute + def public_func, do: :ok + defp private_func_arg(a \\ nil) + defp private_func_arg(a) when is_integer(a), do: :ok + def public_func_arg(b, a \\ "def"), do: :ok + + defmacrop private_macro, do: :ok + defmacro public_macro, do: :ok + + defmacrop private_macro_arg(a), do: :ok + defmacro public_macro_arg(a), do: :ok + + defguardp private_guard when 1 == 1 + defguard public_guard when 1 == 1 + + defguardp private_guard_arg(a) when is_integer(a) + defguard public_guard_arg(a) when is_integer(a) + + defmodule Nested do + def public_func_nested_arg(a), do: :ok + + defmodule Nested.Child do + def public_func_nested_child_arg(a), do: :ok + end + end + + defmodule Elixir.Outside do + def public_func_nested_arg(a), do: :ok + end + + defmodule Deeply.Nested do + def public_func_deeply_nested_arg(a), do: :ok + end + + defprotocol ProtocolEmbedded do + def reverse(term) + end + + defimpl ProtocolEmbedded, for: String do + def reverse(a), do: :ok + end + + defimpl ProtocolOutside, for: String do + def reverse(a), do: :ok + end + + defdelegate delegated_func, to: ElixirSenseExample.Delegates + defoverridable public_func: 0 + @type my_pub_type :: any + @typep my_priv_type :: any + @opaque my_opaque_type :: any + @type my_pub_type_arg(a, b) :: {b, a} + + @callback some_callback(abc) :: :ok when abc: integer + end + end + + defmacro __before_compile__(env) do + IO.puts("BEFORE COMPILE!") + + unless Module.defines?(env.module, {:init, 1}) do + message = """ + function init/1 required by behaviour GenServer is not implemented \ + (in module #{inspect(env.module)}). + We will inject a default implementation for now: + def init(args) do + {:ok, args} + end + You can copy the implementation above or define your own that converts \ + the arguments given to GenServer.start_link/3 to the server state. + """ + + IO.warn(message, Macro.Env.stacktrace(env)) + + quote do + @doc false + def init(args) do + {:ok, args} + end + + defoverridable init: 1 + end + end + end + + @spec reply(from, term) :: :ok + def reply(client, reply) + + def reply({to, tag}, reply) when is_pid(to) do + send(to, {tag, reply}) + :ok + end +end + +defmodule ElixirSenseExample.ExampleBehaviourWithDoc do + @doc "Docs for foo" + @callback foo() :: :ok + + @doc "Docs for baz" + @callback baz(integer()) :: :ok + + @doc "Docs for bar" + @macrocallback bar(integer()) :: Macro.t() +end + +defmodule ElixirSenseExample.ExampleBehaviourWithDocCallbackImpl do + @behaviour ElixirSenseExample.ExampleBehaviourWithDoc + + @impl true + def foo(), do: :ok + + @impl true + def baz(_a), do: :ok + + @impl true + defmacro bar(_b), do: quote(do: :ok) +end + +defmodule ElixirSenseExample.ExampleBehaviourWithDocCallbackNoImpl do + @behaviour ElixirSenseExample.ExampleBehaviourWithDoc + + def foo(), do: :ok + + def baz(_a), do: :ok + + defmacro bar(_b), do: quote(do: :ok) +end + +defmodule ElixirSenseExample.ExampleBehaviourWithDocCallbackErlang do + @behaviour :gen_statem + + def callback_mode, do: :state_functions + + def init(_), do: :ignore +end + +defmodule ElixirSenseExample.ExampleBehaviourWithStruct do + defmacro __using__(opts) do + quote location: :keep, bind_quoted: [opts: opts] do + defstruct [:a, b: 1] + end + end +end + +defmodule ElixirSenseExample.ExampleBehaviourWithException do + defmacro __using__(opts) do + quote location: :keep, bind_quoted: [opts: opts] do + defexception [:a, b: 1] + end + end +end + +defmodule ElixirSenseExample.BehaviourWithMeta do + @doc "Sample doc" + @doc since: "1.2.3" + @callback flatten(list()) :: list() + + @doc "Docs for bar" + @doc since: "1.2.3" + @macrocallback bar(integer()) :: Macro.t() +end + +defmodule ElixirSenseExample.ExampleBehaviourWithDocFalse do + @doc false + @callback foo() :: :ok + + @doc false + @macrocallback bar(integer()) :: Macro.t() +end + +defmodule ElixirSenseExample.ExampleBehaviourWithDocFalseCallbackImpl do + @behaviour ElixirSenseExample.ExampleBehaviourWithDocFalse + + @impl true + def foo(), do: :ok + + @impl true + defmacro bar(_b), do: quote(do: :ok) +end + +defmodule ElixirSenseExample.ExampleBehaviourWithNoDoc do + @callback foo() :: :ok + + @macrocallback bar(integer()) :: Macro.t() +end + +defmodule ElixirSenseExample.ExampleBehaviourWithNoDocCallbackImpl do + @behaviour ElixirSenseExample.ExampleBehaviourWithNoDoc + + @impl true + def foo(), do: :ok + + @impl true + defmacro bar(_b), do: quote(do: :ok) +end diff --git a/apps/language_server/test/support/example_protocol.ex b/apps/language_server/test/support/example_protocol.ex new file mode 100644 index 000000000..0a58af3a8 --- /dev/null +++ b/apps/language_server/test/support/example_protocol.ex @@ -0,0 +1,12 @@ +defprotocol ElixirSenseExample.ExampleProtocol do + @spec some(t) :: any + def some(t) +end + +defimpl ElixirSenseExample.ExampleProtocol, for: List do + def some(t), do: t +end + +defimpl ElixirSenseExample.ExampleProtocol, for: Map do + def some(t), do: t +end diff --git a/apps/language_server/test/support/functions_with_default_args.ex b/apps/language_server/test/support/functions_with_default_args.ex new file mode 100644 index 000000000..2ed194844 --- /dev/null +++ b/apps/language_server/test/support/functions_with_default_args.ex @@ -0,0 +1,54 @@ +defmodule ElixirSenseExample.FunctionsWithDefaultArgs do + @doc "no params version" + @spec my_func :: binary + def my_func, do: "not this one" + + @doc "2 params version" + @spec my_func(1 | 2) :: binary + @spec my_func(1 | 2, binary) :: binary + def my_func(a, b \\ "") + def my_func(1, b), do: "1" <> b + def my_func(2, b), do: "2" <> b + + @doc "3 params version" + @spec my_func(1, 2, 3) :: :ok + def my_func(1, 2, 3), do: :ok + + @spec my_func(2, 2, 3) :: :error + def my_func(2, 2, 3), do: :error +end + +for i <- 1..1 do + defmodule :"Elixir.ElixirSenseExample.FunctionsWithDefaultArgs#{i}" do + @moduledoc "example module" + + @doc "no params version" + @spec my_func :: binary + def my_func, do: "not this one" + + @doc "2 params version" + @spec my_func(1 | 2) :: binary + @spec my_func(1 | 2, binary) :: binary + def my_func(a, b \\ "") + def my_func(1, b), do: "1" <> b + def my_func(2, b), do: "2" <> b + + @doc "3 params version" + @spec my_func(1, 2, 3) :: :ok + def my_func(1, 2, 3), do: :ok + + @spec my_func(2, 2, 3) :: :error + def my_func(2, 2, 3), do: :error + end +end + +defmodule ElixirSenseExample.FunctionsWithDefaultArgsCaller do + alias ElixirSenseExample.FunctionsWithDefaultArgs, as: F + + def go() do + F.my_func() + F.my_func(1) + F.my_func(1, "a") + F.my_func(1, 2, 3) + end +end diff --git a/apps/language_server/test/support/functions_with_return_spec.ex b/apps/language_server/test/support/functions_with_return_spec.ex new file mode 100644 index 000000000..a65a58ffb --- /dev/null +++ b/apps/language_server/test/support/functions_with_return_spec.ex @@ -0,0 +1,125 @@ +defmodule ElixirSenseExample.FunctionsWithReturnSpec do + defstruct [:abc] + + @type t :: %ElixirSenseExample.FunctionsWithReturnSpec{ + abc: %{key: nil} + } + @type x :: %{required(:abc) => atom_1, optional(:cde) => atom_1} + @type atom_1 :: :asd + @type num :: number + @type tup :: {:ok, :abc} + @type int :: 44 + + @spec f01() :: Abc.non_existing() + def f01(), do: :ok + + @spec f02() :: atom_1 + def f02(), do: :ok + + @spec f03() :: num + def f03(), do: :ok + + @spec f04() :: tup + def f04(), do: :ok + + @spec f05() :: int + def f05(), do: :ok + + @spec f1() :: t + def f1(), do: :ok + + @spec f1x(any) :: t + def f1x(_a), do: :ok + + @spec f2() :: x + def f2(), do: :ok + + @spec f3() :: ElixirSenseExample.FunctionsWithReturnSpec.Remote.t() + def f3(), do: :ok + + @spec f4() :: ElixirSenseExample.FunctionsWithReturnSpec.Remote.x() + def f4(), do: :ok + + @spec f5() :: %ElixirSenseExample.FunctionsWithReturnSpec{} + def f5(), do: :ok + + @spec f6() :: %{abc: atom} + def f6(), do: :ok + + @spec f7() :: %{abc: String} + @spec f7() :: nil + def f7(), do: :ok + + @spec f71(integer) :: %{abc: atom} + @spec f71(boolean) :: %{abc: atom} + def f71(_x), do: :ok + + @spec f8() :: %{abc: atom} | nil + def f8(), do: :ok + + @spec f9(a) :: %{abc: atom} when a: integer + def f9(_a), do: :ok + + @spec f91() :: a when a: %{abc: atom} + def f91(), do: :ok + + @spec f10(integer, integer, any) :: String + def f10(_a \\ 0, _b \\ 0, _c), do: String + + @spec f11 :: {:ok, :some} | {:error, :some_error} + def f11, do: {:ok, :some} + + @spec list1 :: [] + def list1, do: [] + + @spec list2 :: list + def list2, do: [] + + @spec list3 :: [...] + def list3, do: [] + + @spec list4 :: [:ok] + def list4, do: [:ok] + + @spec list5 :: list(:ok) + def list5, do: [:ok] + + @spec list6 :: [:ok, ...] + def list6, do: [:ok] + + @spec list7 :: nonempty_list(:ok) + def list7, do: [:ok] + + @spec list8 :: maybe_improper_list(:ok, integer) + def list8, do: [:ok] + + @spec list9 :: nonempty_improper_list(:ok, integer) + def list9, do: [:ok] + + @spec list10 :: nonempty_maybe_improper_list(:ok, integer) + def list10, do: [:ok] + + @spec list11 :: keyword + def list11, do: [some: :ok] + + @spec list12 :: keyword(:ok) + def list12, do: [some: :ok] + + @spec list13 :: [some: :ok] + def list13, do: [some: :ok] + + @spec f_no_return :: no_return + def f_no_return, do: :ok + + @spec f_any :: any + def f_any, do: :ok + + @spec f_term :: term + def f_term, do: :ok +end + +defmodule ElixirSenseExample.FunctionsWithReturnSpec.Remote do + defstruct [:abc] + @type t :: %ElixirSenseExample.FunctionsWithReturnSpec.Remote{} + @type x :: %{abc: atom} +end diff --git a/apps/language_server/test/support/functions_with_the_same_name.ex b/apps/language_server/test/support/functions_with_the_same_name.ex new file mode 100644 index 000000000..f9aedfed2 --- /dev/null +++ b/apps/language_server/test/support/functions_with_the_same_name.ex @@ -0,0 +1,16 @@ +defmodule ElixirSenseExample.FunctionsWithTheSameName do + @doc "all?/2 docs" + def all?(enumerable, fun \\ fn x -> x end) do + IO.inspect({enumerable, fun}) + end + + @doc "concat/1 docs" + def concat(enumerables) do + IO.inspect(enumerables) + end + + @doc "concat/2 docs" + def concat(left, right) do + IO.inspect({left, right}) + end +end diff --git a/apps/language_server/test/support/macro_generated.ex b/apps/language_server/test/support/macro_generated.ex new file mode 100644 index 000000000..e6f1bdf64 --- /dev/null +++ b/apps/language_server/test/support/macro_generated.ex @@ -0,0 +1,14 @@ +defmodule ElixirSenseExample.Macros do + defmacro go do + quote do + @type my_type :: nil + def my_fun(), do: :ok + end + end +end + +defmodule ElixirSenseExample.MacroGenerated do + require ElixirSenseExample.Macros + + ElixirSenseExample.Macros.go() +end diff --git a/apps/language_server/test/support/module_with_builtin_type_shadowing.ex b/apps/language_server/test/support/module_with_builtin_type_shadowing.ex new file mode 100644 index 000000000..815b77878 --- /dev/null +++ b/apps/language_server/test/support/module_with_builtin_type_shadowing.ex @@ -0,0 +1,6 @@ +defmodule ElixirSenseExample.ModuleWithBuiltinTypeShadowing do + @compile {:no_warn_undefined, {B.Callee, :fun, 0}} + def plain_fun do + B.Callee.fun() + end +end diff --git a/apps/language_server/test/support/module_with_functions.ex b/apps/language_server/test/support/module_with_functions.ex new file mode 100644 index 000000000..cb7e5f28e --- /dev/null +++ b/apps/language_server/test/support/module_with_functions.ex @@ -0,0 +1,27 @@ +defmodule ElixirSenseExample.ModuleWithFunctions do + def function_arity_zero do + :return_value + end + + def function_arity_one(_) do + nil + end + + defdelegate delegated_function, to: ElixirSenseExample.ModuleWithFunctions.DelegatedModule + defdelegate delegated_function(a), to: ElixirSenseExample.ModuleWithFunctions.DelegatedModule + defdelegate delegated_function(a, b), to: ElixirSenseExample.ModuleWithFunctions.DelegatedModule + + defmodule DelegatedModule do + def delegated_function do + nil + end + + def delegated_function(a) do + a + end + + def delegated_function(a, b) do + {a, b} + end + end +end diff --git a/apps/language_server/test/support/module_with_many_clauses.ex b/apps/language_server/test/support/module_with_many_clauses.ex new file mode 100644 index 000000000..f249e9376 --- /dev/null +++ b/apps/language_server/test/support/module_with_many_clauses.ex @@ -0,0 +1,12 @@ +defmodule ElixirSenseExample.ModuleWithManyClauses do + def sum(s \\ nil, f) + def sum(a, nil), do: a + + def sum(a, b) do + a + b + end + + def sum({a, b}, x, y) do + a + b + x + y + end +end diff --git a/apps/language_server/test/support/module_with_private_types.ex b/apps/language_server/test/support/module_with_private_types.ex new file mode 100644 index 000000000..5df8c2fac --- /dev/null +++ b/apps/language_server/test/support/module_with_private_types.ex @@ -0,0 +1,10 @@ +defmodule ModuleWithPrivateTypes do + @opaque opaque_t :: atom + @typep typep_t :: atom + @type type_t :: atom + + @spec just_to_use_typep(typep_t) :: typep_t + def just_to_use_typep(t) do + t + end +end diff --git a/apps/language_server/test/support/module_with_record.ex b/apps/language_server/test/support/module_with_record.ex new file mode 100644 index 000000000..23a4d7b54 --- /dev/null +++ b/apps/language_server/test/support/module_with_record.ex @@ -0,0 +1,5 @@ +defmodule ElixirSenseExample.ModuleWithRecord do + require Record + Record.defrecord(:user, name: "john", age: 25) + @type user :: record(:user, name: String.t(), age: integer) +end diff --git a/apps/language_server/test/support/module_with_struct.ex b/apps/language_server/test/support/module_with_struct.ex new file mode 100644 index 000000000..fa0b2bc28 --- /dev/null +++ b/apps/language_server/test/support/module_with_struct.ex @@ -0,0 +1,11 @@ +defmodule ElixirSenseExample.ModuleWithStruct do + defstruct [:field_1, field_2: 1] +end + +defmodule ElixirSenseExample.ModuleWithTypedStruct do + @type t :: %ElixirSenseExample.ModuleWithTypedStruct{ + typed_field: %ElixirSenseExample.ModuleWithStruct{}, + other: integer + } + defstruct [:typed_field, other: 1] +end diff --git a/apps/language_server/test/support/module_with_types.ex b/apps/language_server/test/support/module_with_types.ex new file mode 100644 index 000000000..c6ee4a03f --- /dev/null +++ b/apps/language_server/test/support/module_with_types.ex @@ -0,0 +1,19 @@ +defmodule ElixirSenseExample.ModuleWithTypes do + @type pub_type :: integer + @typep priv_type :: integer + @opaque opaque_type :: priv_type + @callback some_callback(integer) :: atom + @macrocallback some_macrocallback(integer) :: atom + + @spec some_fun_priv(integer) :: integer + defp some_fun_priv(a), do: a + 1 + + @spec some_fun(integer) :: integer + def some_fun(a), do: some_fun_priv(a) + 1 + + @spec some_macro_priv() :: Macro.t() + defmacrop some_macro_priv(), do: :abc + + @spec some_macro(integer) :: Macro.t() + defmacro some_macro(_a), do: some_macro_priv() +end diff --git a/apps/language_server/test/support/module_with_typespecs.ex b/apps/language_server/test/support/module_with_typespecs.ex new file mode 100644 index 000000000..27bc0bb0d --- /dev/null +++ b/apps/language_server/test/support/module_with_typespecs.ex @@ -0,0 +1,193 @@ +defmodule ElixirSenseExample.ModuleWithTypespecs do + defmodule Remote do + @typedoc "Remote type" + @type remote_t :: atom + + @typedoc "Remote type with params" + @type remote_t(a, b) :: {a, b} + + @typedoc "Remote list type" + @type remote_list_t :: [remote_t] + @opaque some_opaque_options_t :: {:atom_opt_1, integer} | {:atom_opt_2, integer} + @type remote_option_t :: {:remote_option_1, remote_t} | {:remote_option_2, remote_list_t} + end + + defmodule OtherRemote do + @type other :: Remote.remote_option_t() + end + + defmodule Local do + alias Remote, as: R + + @typep private_t :: atom + + @typedoc "Local opaque type" + @opaque opaque_t :: atom + + @typedoc "Local type" + @type local_t :: atom + + @typedoc "Local type with params" + @type local_t(a, b) :: {a, b} + + @typedoc "Local union type" + @type union_t :: atom | integer + + @typedoc "Local list type" + @type list_t :: [:trace | :log] + + @typedoc "Local type with large spec" + @type large_t :: pid | port | (registered_name :: atom) | {registered_name :: atom, node} + + @typedoc "Remote type from aliased module" + @type remote_aliased_t :: R.remote_t() | R.remote_list_t() + + @type tuple_opt_t :: {:opt_name, :opt_value} + + @typedoc "Local keyword-value type" + @type option_t :: + {:local_o, local_t} + | {:local_with_params_o, local_t(atom, integer)} + | {:union_o, union_t} + | {:inline_union_o, :a | :b} + | {:list_o, list_t} + | {:inline_list_o, [:trace | :log]} + | {:basic_o, pid} + | {:basic_with_params_o, nonempty_list(atom)} + | {:builtin_o, keyword} + | {:builtin_with_params_o, keyword(term)} + | {:remote_o, Remote.remote_t()} + | {:remote_with_params_o, Remote.remote_t(atom, integer)} + | {:remote_aliased_o, remote_aliased_t} + | {:remote_aliased_inline_o, R.remote_t()} + | {:private_o, private_t} + | {:opaque_o, opaque_t} + | {:non_existent_o, Remote.non_existent()} + | {:large_o, large_t} + + @typedoc "Extra option" + @type extra_option_t :: {:option_1, atom} | {:option_2, integer} + + @typedoc "Options" + @type options_t :: [option_t] + + @typedoc "Option | Extra option" + @type option_or_extra_option_t :: + {:option_1, boolean} | {:option_2, timeout} | Remote.remote_option_t() + + @type extra_option_1_t :: extra_option_t + + @type atom_opt_t :: :atom_opt + + @opaque some_opaque_options_t :: {:atom_opt_1, integer} | {:atom_opt_2, integer} + + @spec func_with_options(options_t) :: any + def func_with_options(options) do + options + end + + @spec func_with_union_of_options([option_t | extra_option_t]) :: any + def func_with_union_of_options(options) do + options + end + + @spec func_with_union_of_options_as_type([option_or_extra_option_t]) :: any + def func_with_union_of_options_as_type(options) do + options + end + + @spec func_with_union_of_options_inline([{:option_1, atom} | {:option_2, integer} | option_t]) :: + any + def func_with_union_of_options_inline(options) do + options + end + + @spec func_with_named_options(options :: options_t) :: any + def func_with_named_options(options) do + options + end + + @spec func_with_options_as_inline_list([{:local_o, local_t} | {:builtin_o, keyword}]) :: any + def func_with_options_as_inline_list(options) do + options + end + + @spec func_with_option_var_defined_in_when([opt]) :: any when opt: option_t + def func_with_option_var_defined_in_when(options) do + options + end + + @spec func_with_options_var_defined_in_when(opts) :: any when opts: [option_t] + def func_with_options_var_defined_in_when(options) do + options + end + + @spec func_with_one_option([{:option_1, integer}]) :: any + def func_with_one_option(options) do + options + end + + @spec fun_without_options([integer]) :: integer + def fun_without_options(a), do: length(a) + + @spec fun_with_atom_option([:option_name]) :: any + def fun_with_atom_option(a), do: a + + @spec fun_with_atom_option_in_when(opts) :: any when opts: [:option_name] + def fun_with_atom_option_in_when(a), do: a + + @spec fun_with_recursive_remote_type_option([OtherRemote.other()]) :: any + def fun_with_recursive_remote_type_option(a), do: a + + @spec fun_with_recursive_user_type_option([extra_option_1_t]) :: any + def fun_with_recursive_user_type_option(a), do: a + + @spec fun_with_tuple_option_in_when(opt) :: any when opt: [tuple_opt_t] + def fun_with_tuple_option_in_when(a), do: a + + @spec fun_with_tuple_option([tuple_opt_t]) :: any + def fun_with_tuple_option(a), do: a + + @spec fun_with_atom_user_type_option_in_when(opt) :: any when opt: [atom_opt_t] + def fun_with_atom_user_type_option_in_when(a), do: a + + @spec fun_with_atom_user_type_option([atom_opt_t]) :: any + def fun_with_atom_user_type_option(a), do: a + + @spec fun_with_list_of_lists([opt]) :: any when opt: [tuple_opt_t] + def fun_with_list_of_lists(a), do: a + + @spec fun_with_recursive_type(opt) :: any when opt: [term :: opt] + def fun_with_recursive_type(a), do: a + + @spec fun_with_multiple_specs(nil) :: any + @spec fun_with_multiple_specs([tuple_opt_t]) :: any + def fun_with_multiple_specs(a), do: a + + @spec fun_with_multiple_specs_when(nil) :: any + @spec fun_with_multiple_specs_when([opts]) :: any when opts: tuple_opt_t + def fun_with_multiple_specs_when(a), do: a + + @spec fun_with_local_opaque([some_opaque_options_t]) :: any + def fun_with_local_opaque(a), do: a + + @spec fun_with_remote_opaque([Remote.some_opaque_options_t()]) :: any + def fun_with_remote_opaque(a), do: a + + @spec func_with_edoc_options([{:edoc_t, :docsh_edoc_xmerl.xml_element_content()}]) :: any + def func_with_edoc_options(options) do + options + end + + @spec func_with_erlang_type_options([{:erlang_t, :erlang.time_unit()}]) :: any + def func_with_erlang_type_options(options) do + options + end + + @spec macro_with_options(options_t) :: Macro.t() + defmacro macro_with_options(options) do + IO.inspect(options) + {:asd, [], nil} + end + end +end diff --git a/apps/language_server/test/support/modules_with_docs.ex b/apps/language_server/test/support/modules_with_docs.ex new file mode 100644 index 000000000..f8759d7d8 --- /dev/null +++ b/apps/language_server/test/support/modules_with_docs.ex @@ -0,0 +1,123 @@ +defmodule ElixirSenseExample.ModuleWithDocs do + @moduledoc """ + An example module + """ + @moduledoc since: "1.2.3" + + @typedoc """ + An example type + """ + @typedoc since: "1.1.0" + @type some_type :: integer + @typedoc false + @type some_type_doc_false :: integer + @type some_type_no_doc :: integer + + @typedoc """ + An example opaque type + """ + @opaque opaque_type :: integer + + @doc """ + An example fun + """ + @doc since: "1.1.0" + def some_fun(a, b \\ nil), do: a + b + @doc false + def some_fun_doc_false(a, b \\ nil), do: a + b + def some_fun_no_doc(a, b \\ nil), do: a + b + + @doc """ + An example macro + """ + @doc since: "1.1.0" + defmacro some_macro(a, b \\ nil), do: a + b + @doc false + defmacro some_macro_doc_false(a, b \\ nil), do: a + b + defmacro some_macro_no_doc(a, b \\ nil), do: a + b + + @doc """ + An example callback + """ + @doc since: "1.1.0" + @callback some_callback(integer) :: atom + @doc false + @callback some_callback_doc_false(integer) :: atom + @callback some_callback_no_doc(integer) :: atom + + @doc """ + An example callback + """ + @doc since: "1.1.0" + @macrocallback some_macrocallback(integer) :: atom + @doc false + @macrocallback some_macrocallback_doc_false(integer) :: atom + @macrocallback some_macrocallback_no_doc(integer) :: atom + + @doc """ + An example fun + """ + @doc deprecated: "This function will be removed in a future release" + def soft_deprecated_fun(_a), do: :ok + + @doc """ + An example macro + """ + @doc deprecated: "This macro will be removed in a future release" + defmacro soft_deprecated_macro(_a), do: :ok + + # As of elixir 1.10 hard deprecation by @deprecated attribute is only supported for macros and functions + + @doc """ + An example fun + """ + @deprecated "This function will be removed in a future release" + def hard_deprecated_fun(_a), do: :ok + + @doc """ + An example macro + """ + @deprecated "This macro will be removed in a future release" + defmacro hard_deprecated_macro(_a), do: :ok + + @doc """ + An example callback + """ + @doc deprecated: "This callback will be removed in a future release" + @callback soft_deprecated_callback(integer) :: atom + + @doc """ + An example macrocallback + """ + @doc deprecated: "This callback will be removed in a future release" + @macrocallback soft_deprecated_macrocallback(integer) :: atom + + @typedoc """ + An example type + """ + @typedoc deprecated: "This type will be removed in a future release" + @type soft_deprecated_type :: integer + + @optional_callbacks soft_deprecated_callback: 1, soft_deprecated_macrocallback: 1 +end + +defmodule ElixirSenseExample.ModuleWithDocFalse do + @moduledoc false +end + +defmodule ElixirSenseExample.ModuleWithNoDocs do +end + +defmodule ElixirSenseExample.SoftDeprecatedModule do + @moduledoc """ + An example module + """ + @moduledoc deprecated: "This module will be removed in a future release" +end + +defmodule ElixirSenseExample.ModuleWithDelegates do + @doc """ + A delegated function + """ + defdelegate delegated_fun(a, b), to: ElixirSenseExample.ModuleWithDocs, as: :some_fun_no_doc +end diff --git a/apps/language_server/test/support/modules_with_references.ex b/apps/language_server/test/support/modules_with_references.ex new file mode 100644 index 000000000..c030212e5 --- /dev/null +++ b/apps/language_server/test/support/modules_with_references.ex @@ -0,0 +1,117 @@ +defmodule ElixirSense.Providers.ReferencesTest.Modules do + defmodule Callee1 do + def func() do + IO.puts("") + end + + def func(par1) do + IO.puts(par1) + end + end + + defmodule Callee2 do + def func() do + IO.puts("") + end + end + + defmodule Callee3 do + def func() do + IO.puts("") + end + end + + defmodule Callee4 do + def func_no_arg() do + IO.puts("") + end + + def func_arg(arg \\ "") do + IO.puts("" <> arg) + end + end + + defmodule Caller1 do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee1.func() + end + end + + defmodule Caller2 do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee1.func("test") + end + end + + defmodule Caller3 do + def func() do + "test" + |> ElixirSense.Providers.ReferencesTest.Modules.Callee4.func_arg() + end + end + + defmodule Caller4 do + def func() do + Task.start(&ElixirSense.Providers.ReferencesTest.Modules.Callee4.func_no_arg/0) + end + end + + defmodule CallerWithAliasesAndImports do + alias ElixirSense.Providers.ReferencesTest.Modules.Callee1 + alias ElixirSense.Providers.ReferencesTest.Modules.Callee2, as: AliasedCallee2 + import ElixirSense.Providers.ReferencesTest.Modules.Callee3 + + def call_all() do + [Callee1.func(), AliasedCallee2.func(), func(), Callee1.func(), Callee1.func("1")] + end + + def call_on_different_line() do + Callee3. + func() + end + + def call_erlang() do + :ets.new(:a, []) + end + end + + defmodule Callee5 do + def func_arg(arg \\ "") do + IO.puts("" <> arg) + end + + def func_arg1(arg \\ "") do + IO.puts("" <> arg) + end + end + + defmodule Caller5 do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee5.func_arg() + ElixirSense.Providers.ReferencesTest.Modules.Callee5.func_arg1("a") + end + end + + defmodule Callee6 do + end + + defmodule Caller6 do + def func() do + ElixirSense.Providers.ReferencesTest.Modules.Callee6.__info__(:function) + ElixirSense.Providers.ReferencesTest.Modules.Callee6.module_info() + end + end + + defmodule Callee7 do + def func_noarg() do + IO.puts("") + end + end + + defmodule Caller7 do + @attr ElixirSense.Providers.ReferencesTest.Modules.Callee7 + def func() do + @attr.func_noarg() + end + end +end diff --git a/apps/language_server/test/support/overridable_function.ex b/apps/language_server/test/support/overridable_function.ex new file mode 100644 index 000000000..6fab46b36 --- /dev/null +++ b/apps/language_server/test/support/overridable_function.ex @@ -0,0 +1,61 @@ +defmodule ElixirSenseExample.OverridableFunctions do + defmacro __using__(_opts) do + quote do + @doc "Some overridable" + @doc since: "1.2.3" + @spec test(number, number) :: number + def test(x, y) do + x + y + end + + defmacro required(var), do: Macro.expand(var, __CALLER__) + + defoverridable test: 2, required: 1 + end + end +end + +defmodule ElixirSenseExample.OverridableBehaviour do + @callback foo :: any + @macrocallback bar(any) :: Macro.t() +end + +defmodule ElixirSenseExample.OverridableImplementation do + alias ElixirSenseExample.OverridableBehaviour + + defmacro __using__(_opts) do + quote do + @behaviour OverridableBehaviour + + def foo do + "Override me" + end + + defmacro bar(var), do: Macro.expand(var, __CALLER__) + + defoverridable OverridableBehaviour + end + end +end + +defmodule ElixirSenseExample.OverridableImplementation.Overrider do + use ElixirSenseExample.OverridableImplementation + + def foo do + super() + end + + defmacro bar(any) do + super(any) + end +end + +defmodule ElixirSenseExample.Overridable.Using do + alias ElixirSenseExample.OverridableImplementation + + defmacro __using__(_opts) do + quote do + use OverridableImplementation + end + end +end diff --git a/apps/language_server/test/support/plugins/ecto/fake_schemas.ex b/apps/language_server/test/support/plugins/ecto/fake_schemas.ex new file mode 100644 index 000000000..006f15ae9 --- /dev/null +++ b/apps/language_server/test/support/plugins/ecto/fake_schemas.ex @@ -0,0 +1,77 @@ +defmodule ElixirSense.Plugins.Ecto.FakeSchemas.User do + @moduledoc """ + Fake User schema. + + The docs. + """ + + def __schema__(:fields), do: [:id, :name, :email] + def __schema__(:associations), do: [:assoc1, :assoc2] + def __schema__(:type, :id), do: :id + def __schema__(:type, :name), do: :string + def __schema__(:type, :email), do: :string + + def __schema__(:association, :assoc1), + do: %{related: FakeAssoc1, owner: __MODULE__, owner_key: :assoc1_id} + + def __schema__(:association, :assoc2), + do: %{related: FakeAssoc2, owner: __MODULE__, owner_key: :assoc2_id} +end + +defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Comment do + @moduledoc """ + Fake Comment schema. + """ + + alias ElixirSense.Plugins.Ecto.FakeSchemas.Post + + def __schema__(:fields), do: [:content, :date] + def __schema__(:associations), do: [:post] + def __schema__(:type, :content), do: :string + def __schema__(:type, :date), do: :date + + def __schema__(:association, :post), + do: %{related: Post, owner: __MODULE__, owner_key: :post_id} +end + +defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Post do + @moduledoc """ + Fake Post schema. + """ + + alias ElixirSense.Plugins.Ecto.FakeSchemas.User + alias ElixirSense.Plugins.Ecto.FakeSchemas.Comment + + def __schema__(:fields), do: [:id, :title, :text, :date, :user_id] + def __schema__(:associations), do: [:user, :comments, :tags] + def __schema__(:type, :id), do: :id + def __schema__(:type, :user_id), do: :id + def __schema__(:type, :title), do: :string + def __schema__(:type, :text), do: :string + def __schema__(:type, :date), do: :date + + def __schema__(:association, :user), + do: %{related: User, related_key: :id, owner: __MODULE__, owner_key: :user_id} + + def __schema__(:association, :comments), + do: %{related: Comment, related_key: :post_id, owner: __MODULE__, owner_key: :id} + + def __schema__(:association, :tags), + do: %{related: Tag, owner: __MODULE__, owner_key: :id} +end + +defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Tag do + @moduledoc """ + Fake Tag schema. + """ + + alias ElixirSense.Plugins.Ecto.FakeSchemas.Post + + def __schema__(:fields), do: [:id, :name] + def __schema__(:associations), do: [:posts] + def __schema__(:type, :id), do: :id + def __schema__(:type, :name), do: :string + + def __schema__(:association, :posts), + do: %{related: Post, owner: __MODULE__, owner_key: :id} +end diff --git a/apps/language_server/test/support/plugins/ecto/migration.ex b/apps/language_server/test/support/plugins/ecto/migration.ex new file mode 100644 index 000000000..c733a4544 --- /dev/null +++ b/apps/language_server/test/support/plugins/ecto/migration.ex @@ -0,0 +1,12 @@ +defmodule Ecto.Migration do + @moduledoc ~S""" + Fake Migration module. + """ + + @doc """ + Defines a field on the schema with given name and type. + """ + def add(column, type, opts \\ []) when is_atom(column) and is_list(opts) do + {column, type, opts} + end +end diff --git a/apps/language_server/test/support/plugins/ecto/query.ex b/apps/language_server/test/support/plugins/ecto/query.ex new file mode 100644 index 000000000..c9696ac0c --- /dev/null +++ b/apps/language_server/test/support/plugins/ecto/query.ex @@ -0,0 +1,117 @@ +defmodule Ecto.Query do + @moduledoc ~S""" + Fake Query module. + """ + + @doc """ + Creates a query. + """ + defmacro from(expr, kw \\ []) do + {expr, kw} + end + + @doc """ + A select query expression. + + Selects which fields will be selected from the schema and any transformations + that should be performed on the fields. Any expression that is accepted in a + query can be a select field. + + ## Keywords examples + + from(c in City, select: c) # returns the schema as a struct + from(c in City, select: {c.name, c.population}) + from(c in City, select: [c.name, c.county]) + + It is also possible to select a struct and limit the returned + fields at the same time: + + from(City, select: [:name]) + + The syntax above is equivalent to: + + from(city in City, select: struct(city, [:name])) + + ## Expressions examples + + City |> select([c], c) + City |> select([c], {c.name, c.country}) + City |> select([c], %{"name" => c.name}) + + """ + defmacro select(query, binding \\ [], expr) do + {query, binding, expr} + end + + @doc """ + Mergeable select query expression. + + This macro is similar to `select/3` except it may be specified + multiple times as long as every entry is a map. This is useful + for merging and composing selects. For example: + + query = from p in Post, select: %{} + + query = + if include_title? do + from p in query, select_merge: %{title: p.title} + else + query + end + + query = + if include_visits? do + from p in query, select_merge: %{visits: p.visits} + else + query + end + + In the example above, the query is built little by little by merging + into a final map. If both conditions above are true, the final query + would be equivalent to: + + from p in Post, select: %{title: p.title, visits: p.visits} + + If `:select_merge` is called and there is no value selected previously, + it will default to the source, `p` in the example above. + """ + defmacro select_merge(query, binding \\ [], expr) do + {query, binding, expr} + end + + @doc """ + A distinct query expression. + + When true, only keeps distinct values from the resulting + select expression. + + ## Keywords examples + + # Returns the list of different categories in the Post schema + from(p in Post, distinct: true, select: p.category) + + # If your database supports DISTINCT ON(), + # you can pass expressions to distinct too + from(p in Post, + distinct: p.category, + order_by: [p.date]) + + # The DISTINCT ON() also supports ordering similar to ORDER BY. + from(p in Post, + distinct: [desc: p.category], + order_by: [p.date]) + + # Using atoms + from(p in Post, distinct: :category, order_by: :date) + + ## Expressions example + + Post + |> distinct(true) + |> order_by([p], [p.category, p.author]) + + """ + defmacro distinct(query, binding \\ [], expr) do + {query, binding, expr} + end +end diff --git a/apps/language_server/test/support/plugins/ecto/schema.ex b/apps/language_server/test/support/plugins/ecto/schema.ex new file mode 100644 index 000000000..ecf5a38bc --- /dev/null +++ b/apps/language_server/test/support/plugins/ecto/schema.ex @@ -0,0 +1,32 @@ +defmodule Ecto.Schema do + @moduledoc ~S""" + Fake Schema module. + """ + + @doc """ + Defines a field on the schema with given name and type. + """ + defmacro field(name, type \\ :string, opts \\ []) do + {name, type, opts} + end + + defmacro schema(source, do: block) do + {source, block} + end + + defmacro has_many(name, queryable, opts \\ []) do + {name, queryable, opts} + end + + defmacro has_one(name, queryable, opts \\ []) do + {name, queryable, opts} + end + + defmacro belongs_to(name, queryable, opts \\ []) do + {name, queryable, opts} + end + + defmacro many_to_many(name, queryable, opts \\ []) do + {name, queryable, opts} + end +end diff --git a/apps/language_server/test/support/plugins/ecto/uuid.ex b/apps/language_server/test/support/plugins/ecto/uuid.ex new file mode 100644 index 000000000..050cc02d7 --- /dev/null +++ b/apps/language_server/test/support/plugins/ecto/uuid.ex @@ -0,0 +1,19 @@ +defmodule Ecto.Type do + @moduledoc """ + Fake Ecto.Type + """ + + @callback fake :: true +end + +defmodule Ecto.UUID do + @moduledoc """ + Fake Ecto.UUID + """ + + @behaviour Ecto.Type + + def fake() do + true + end +end diff --git a/apps/language_server/test/support/plugins/phoenix/page_controller.ex b/apps/language_server/test/support/plugins/phoenix/page_controller.ex new file mode 100644 index 000000000..3c70e5065 --- /dev/null +++ b/apps/language_server/test/support/plugins/phoenix/page_controller.ex @@ -0,0 +1,10 @@ +defmodule ExampleWeb.PageController do + def call(_conn, _params) do + end + + def action(_conn, _params) do + end + + def home(_conn, _params) do + end +end diff --git a/apps/language_server/test/support/plugins/phoenix/router.ex b/apps/language_server/test/support/plugins/phoenix/router.ex new file mode 100644 index 000000000..fa0d3064e --- /dev/null +++ b/apps/language_server/test/support/plugins/phoenix/router.ex @@ -0,0 +1,4 @@ +defmodule Phoenix.Router do + def get(_route, _plug, _plut_opts, _opts \\ []) do + end +end diff --git a/apps/language_server/test/support/references_tracer.ex b/apps/language_server/test/support/references_tracer.ex new file mode 100644 index 000000000..63b7f374e --- /dev/null +++ b/apps/language_server/test/support/references_tracer.ex @@ -0,0 +1,57 @@ +defmodule ElixirSense.Core.References.Tracer do + @moduledoc """ + Elixir Compiler tracer that registers function calls + """ + use Agent + + @spec start_link(ElixirSense.call_trace_t()) :: Agent.on_start() + def start_link(initial \\ %{}) do + Agent.start_link(fn -> initial end, name: __MODULE__) + end + + @spec get :: ElixirSense.call_trace_t() + def get do + Agent.get(__MODULE__, & &1) + end + + @spec register_call(ElixirSense.call_t()) :: :ok + def register_call(%{callee: callee} = call) do + Agent.update(__MODULE__, fn calls -> + updated_calls = + case calls[callee] do + nil -> [call] + callee_calls -> [call | callee_calls] + end + + calls |> Map.put(callee, updated_calls) + end) + end + + def trace({kind, meta, module, name, arity}, env) + when kind in [:imported_function, :imported_macro, :remote_function, :remote_macro] do + register_call(%{ + callee: {module, name, arity}, + file: env.file |> Path.relative_to_cwd(), + line: meta[:line], + column: meta[:column] + }) + + :ok + end + + def trace({kind, meta, name, arity}, env) + when kind in [:local_function, :local_macro] do + register_call(%{ + callee: {env.module, name, arity}, + file: env.file |> Path.relative_to_cwd(), + line: meta[:line], + column: meta[:column] + }) + + :ok + end + + def trace(_trace, _env) do + :ok + end +end diff --git a/apps/language_server/test/support/same_module.ex b/apps/language_server/test/support/same_module.ex new file mode 100644 index 000000000..e00af5036 --- /dev/null +++ b/apps/language_server/test/support/same_module.ex @@ -0,0 +1,9 @@ +defmodule ElixirSenseExample.SameModule do + def test_fun(), do: :ok + + defmacro some_test_macro() do + quote do + @attr "val" + end + end +end diff --git a/apps/language_server/test/support/stuct_with_typespec.ex b/apps/language_server/test/support/stuct_with_typespec.ex new file mode 100644 index 000000000..a9b8e97fd --- /dev/null +++ b/apps/language_server/test/support/stuct_with_typespec.ex @@ -0,0 +1,13 @@ +defmodule ElixirSenseExample.IO.Stream do + defstruct [ + :device, + :line_or_bytes, + :raw + ] + + @type t() :: %ElixirSenseExample.IO.Stream{ + device: IO.device(), + line_or_bytes: :line | non_neg_integer(), + raw: boolean() + } +end diff --git a/apps/language_server/test/support/subscriber.ex b/apps/language_server/test/support/subscriber.ex new file mode 100644 index 000000000..f2c8c59ce --- /dev/null +++ b/apps/language_server/test/support/subscriber.ex @@ -0,0 +1,6 @@ +defmodule ElixirSenseExample.Subscriber do + def some do + ElixirSenseExample.Subscription.check("user", [:a, :b], :c) + ElixirSenseExample.Subscription.check("user", [:a, :b], :c, :s) + end +end diff --git a/apps/language_server/test/support/subscription.ex b/apps/language_server/test/support/subscription.ex new file mode 100644 index 000000000..d36e48a65 --- /dev/null +++ b/apps/language_server/test/support/subscription.ex @@ -0,0 +1,11 @@ +defmodule ElixirSenseExample.Subscription do + def check(resource, models, user, opts \\ []) + + def check(nil, models, user, opts) do + IO.inspect({nil, models, user, opts}) + end + + def check(resource, models, user, opts) do + IO.inspect({resource, models, user, opts}) + end +end diff --git a/apps/language_server/test/support/types_with_multiple_arity.ex b/apps/language_server/test/support/types_with_multiple_arity.ex new file mode 100644 index 000000000..02072c807 --- /dev/null +++ b/apps/language_server/test/support/types_with_multiple_arity.ex @@ -0,0 +1,19 @@ +defmodule ElixirSenseExample.TypesWithMultipleArity do + @typedoc "no params version" + @type my_type :: integer + @typedoc "one param version" + @type my_type(a) :: {integer, a} + @typedoc "two params version" + @type my_type(a, b) :: {integer, a, b} +end + +for i <- 1..1 do + defmodule :"Elixir.ElixirSenseExample.TypesWithMultipleArity#{i}" do + @typedoc "no params version" + @type my_type :: integer + @typedoc "one param version" + @type my_type(a) :: {integer, a} + @typedoc "two params version" + @type my_type(a, b) :: {integer, a, b} + end +end diff --git a/apps/language_server/test/support/use_example.ex b/apps/language_server/test/support/use_example.ex new file mode 100644 index 000000000..bdffaaacf --- /dev/null +++ b/apps/language_server/test/support/use_example.ex @@ -0,0 +1,9 @@ +defmodule ElixirSenseExample.UseExample do + defmacro __using__(_) do + quote do + def example do + 42 + end + end + end +end diff --git a/apps/language_server/test/test_helper.exs b/apps/language_server/test/test_helper.exs index f29e4f72e..0eaf4e09a 100644 --- a/apps/language_server/test/test_helper.exs +++ b/apps/language_server/test/test_helper.exs @@ -1,3 +1,3 @@ :persistent_term.put(:language_server_test_mode, true) Application.ensure_started(:stream_data) -ExUnit.start(exclude: [pending: true]) +ExUnit.start(exclude: [pending: true, requires_source: true])