Skip to content

Commit

Permalink
Introduce typespec scope (#231)
Browse files Browse the repository at this point in the history
* record typespec scope

* do not find types in function and module body

do not find functions in typespecs

* add failing test

* pass scope

* correctly find typespecs and locals

* track specs

* fix last tests

handle incomplete spec

* add missing clause

* fix crash when variable not found

* fallback to local fun

* fix test

* format

* fix credo issues

* fix dialyzer

* add test
  • Loading branch information
lukaszsamson authored Jun 8, 2023
1 parent c9ccb3f commit a6abc22
Show file tree
Hide file tree
Showing 18 changed files with 605 additions and 189 deletions.
94 changes: 65 additions & 29 deletions lib/elixir_sense/core/introspection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,29 @@ defmodule ElixirSense.Core.Introspection do
Keyword.has_key?(get_exports(module), fun)
end

@spec get_all_docs(mod_fun, ElixirSense.Core.State.scope()) :: docs
def get_all_docs({mod, nil}, _) do
def get_all_docs({mod, nil}, :mod_fun, _) do
%{docs: get_docs_md(mod), types: get_types_md(mod), callbacks: get_callbacks_md(mod)}
end

def get_all_docs({mod, fun}, scope) do
def get_all_docs({mod, fun}, :mod_fun, _scope) do
docs =
with(
[] <- get_func_docs_md(mod, fun),
[] <- get_type_docs_md(mod, fun, scope)
) do
nil
else
case get_func_docs_md(mod, fun) do
[] ->
nil

docs ->
Enum.join(docs, "\n\n---\n\n") <> "\n"
end

%{docs: docs, types: get_types_md(mod)}
end

def get_all_docs({mod, fun}, :type, scope) do
docs =
case get_type_docs_md(mod, fun, scope) do
[] ->
nil

docs ->
Enum.join(docs, "\n\n---\n\n") <> "\n"
end
Expand Down Expand Up @@ -1100,16 +1110,19 @@ defmodule ElixirSense.Core.Introspection do
nil | module,
[{module, module}],
nil | module,
# TODO
any,
ElixirSense.Core.State.mods_funs_to_positions_t()
) :: {nil | module, boolean}
def actual_module(module, aliases, current_module, mods_funs) do
{m, nil, res} =
def actual_module(module, aliases, current_module, scope, mods_funs) do
{m, nil, res, _} =
actual_mod_fun(
{module, nil},
[],
[],
aliases,
current_module,
scope,
mods_funs,
%{}
)
Expand Down Expand Up @@ -1184,40 +1197,46 @@ defmodule ElixirSense.Core.Introspection do
[module],
[{module, module}],
nil | module,
# TODO
any(),
ElixirSense.Core.State.mods_funs_to_positions_t(),
ElixirSense.Core.State.types_t()
) :: {nil | module, nil | atom, boolean}
def actual_mod_fun({nil, nil}, _, _, _, _, _, _), do: {nil, nil, false}
) :: {nil | module, nil | atom, boolean, nil | :mod_fun | :type}
def actual_mod_fun({nil, nil}, _, _, _, _, _, _, _), do: {nil, nil, false, nil}

def actual_mod_fun(
{mod, fun} = mod_fun,
imports,
requires,
aliases,
current_module,
scope,
mods_funs,
metadata_types
) do
expanded_mod = expand_alias(mod, aliases)

with {nil, nil} <- find_kernel_special_forms_macro(mod_fun),
{nil, nil} <-
find_function_or_module(
{expanded_mod, fun},
current_module,
imports,
requires,
mods_funs
),
{nil, nil} <- find_type({expanded_mod, fun}, current_module, metadata_types) do
{expanded_mod, fun, false}
with {:mod_fun, {nil, nil}} <- {:mod_fun, find_kernel_special_forms_macro(mod_fun)},
{:mod_fun, {nil, nil}} <-
{:mod_fun,
find_function_or_module(
{expanded_mod, fun},
current_module,
scope,
imports,
requires,
mods_funs
)},
{:type, {nil, nil}} <-
{:type, find_type({expanded_mod, fun}, current_module, scope, metadata_types)} do
{expanded_mod, fun, false, nil}
else
{m, f} -> {m, f, true}
{kind, {m, f}} -> {m, f, true, kind}
end
end

# local type
defp find_type({nil, type}, current_module, metadata_types) do
defp find_type({nil, type}, current_module, {:typespec, _, _}, metadata_types) do
case metadata_types[{current_module, type, nil}] do
nil ->
if BuiltinTypes.builtin_type?(type) do
Expand All @@ -1232,13 +1251,13 @@ defmodule ElixirSense.Core.Introspection do
end

# Elixir proxy
defp find_type({Elixir, _type}, _current_module, _metadata_types), do: {nil, nil}
defp find_type({Elixir, _type}, _current_module, _scope, _metadata_types), do: {nil, nil}

# invalid case
defp find_type({_mod, nil}, _current_module, _metadata_types), do: {nil, nil}
defp find_type({_mod, nil}, _current_module, _scope, _metadata_types), do: {nil, nil}

# remote type
defp find_type({mod, type}, _current_module, metadata_types) do
defp find_type({mod, type}, _current_module, {:typespec, _, _}, metadata_types) do
found =
case metadata_types[{mod, type, nil}] do
nil ->
Expand All @@ -1258,10 +1277,24 @@ defmodule ElixirSense.Core.Introspection do
end
end

defp find_type({_mod, _type}, _current_module, _scope, _metadata_types), do: {nil, nil}

defp find_function_or_module(
{_mod, fun},
_current_module,
{:typespec, _, _},
_imports,
_requires,
_mods_funs
)
when fun != nil,
do: {nil, nil}

# local call
defp find_function_or_module(
{nil, fun},
current_module,
_scope,
imports,
_requires,
mods_funs
Expand Down Expand Up @@ -1298,6 +1331,7 @@ defmodule ElixirSense.Core.Introspection do
defp find_function_or_module(
{Elixir, _},
_current_module,
_scope,
_imports,
_requires,
_mods_funs
Expand All @@ -1308,6 +1342,7 @@ defmodule ElixirSense.Core.Introspection do
defp find_function_or_module(
{mod, nil},
_current_module,
_scope,
_imports,
_requires,
mods_funs
Expand All @@ -1324,6 +1359,7 @@ defmodule ElixirSense.Core.Introspection do
{mod, fun},
_current_module,
_imports,
_scope,
requires,
mods_funs
) do
Expand Down
76 changes: 74 additions & 2 deletions lib/elixir_sense/core/metadata_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,9 @@ defmodule ElixirSense.Core.MetadataBuilder do
spec = TypeInfo.typespec_to_string(kind, spec)

state
|> add_current_env_to_line(line)
|> add_type(type_name, type_args, spec, kind, pos)
|> add_typespec_namespace(type_name, length(type_args))
|> add_current_env_to_line(line)
|> result(ast)
end

Expand All @@ -415,8 +416,9 @@ defmodule ElixirSense.Core.MetadataBuilder do
end

state
|> add_current_env_to_line(line)
|> add_spec(type_name, type_args, spec, kind, pos)
|> add_typespec_namespace(type_name, length(type_args))
|> add_current_env_to_line(line)
|> result(ast)
end

Expand Down Expand Up @@ -666,6 +668,26 @@ defmodule ElixirSense.Core.MetadataBuilder do
)
end

# incomplete spec
# @callback my(integer)
defp pre(
{:@, [line: line, column: column] = _meta_attr,
[{kind, _, [{name, _, type_args}]} = spec]} = ast,
state
)
when kind in [:spec, :callback, :macrocallback] and is_atom(name) and
(is_nil(type_args) or is_list(type_args)) do
pre_spec(
ast,
state,
{line, column},
name,
expand_aliases_in_ast(state, List.wrap(type_args)),
expand_aliases_in_ast(state, spec),
kind
)
end

defp pre({:@, [line: line, column: column] = meta_attr, [{name, meta, params}]}, state) do
{type, is_definition} =
case List.wrap(params) do
Expand Down Expand Up @@ -1185,6 +1207,56 @@ defmodule ElixirSense.Core.MetadataBuilder do
{ast, state}
end

defp post(
{:@, _meta_attr,
[{kind, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = _spec]}]} =
ast,
state
)
when kind in [:type, :typep, :opaque] and is_atom(name) and
(is_nil(type_args) or is_list(type_args)) do
state =
state
|> remove_last_scope_from_scopes

{ast, state}
end

defp post(
{:@, _meta_attr,
[
{kind, _,
[
{:when, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]}, _]} =
_spec
]}
]} = ast,
state
)
when kind in [:spec, :callback, :macrocallback] and is_atom(name) and
(is_nil(type_args) or is_list(type_args)) do
state =
state
|> remove_last_scope_from_scopes

{ast, state}
end

defp post(
{:@, _meta_attr,
[{kind, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = _spec]}]} =
ast,
state
)
when kind in [:spec, :callback, :macrocallback] and is_atom(name) and
(is_nil(type_args) or is_list(type_args)) do
state =
state
|> remove_last_scope_from_scopes

{ast, state}
end

defp post(
{:case, _meta,
[
Expand Down
2 changes: 1 addition & 1 deletion lib/elixir_sense/core/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ defmodule ElixirSense.Core.Source do

# since elixir 1.15 previous lines are kept in blocks
# https://github.com/elixir-lang/elixir/commit/faf81cd92c7d6668d2e8115744cc8d06f9bfecba
# skip as __block__ is not a call we are interested in
# skip as __block__ is not a call we are interested in
@excluded_funs [:__block__]

@spec which_func(String.t(), nil | %Binding{}) ::
Expand Down
13 changes: 12 additions & 1 deletion lib/elixir_sense/core/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule ElixirSense.Core.State do
alias ElixirSense.Core.Introspection

@type fun_arity :: {atom, non_neg_integer}
@type scope :: atom | fun_arity
@type scope :: atom | fun_arity | {:typespec, atom, non_neg_integer}

@type alias_t :: {module, module}
@type scope_id_t :: non_neg_integer
Expand Down Expand Up @@ -453,6 +453,7 @@ defmodule ElixirSense.Core.State do

def get_current_scope_name(%__MODULE__{} = state) do
case hd(hd(state.scopes)) do
{:typespec, fun, _} -> fun |> Atom.to_string()
{fun, _} -> fun |> Atom.to_string()
mod -> mod |> Atom.to_string()
end
Expand Down Expand Up @@ -633,6 +634,16 @@ defmodule ElixirSense.Core.State do
%{state | namespace: outer_mods, scopes: outer_scopes}
end

def add_typespec_namespace(%__MODULE__{} = state, name, arity) do
%{state | scopes: [[{:typespec, name, arity} | hd(state.scopes)] | state.scopes]}
end

# def remove_typespec_namespace(%__MODULE__{} = state) do
# outer_scopes = state.scopes |> tl

# %{state | scopes: outer_scopes}
# end

def new_named_func(%__MODULE__{} = state, name, arity) do
%{state | scopes: [[{name, arity} | hd(state.scopes)] | state.scopes]}
end
Expand Down
2 changes: 1 addition & 1 deletion lib/elixir_sense/plugins/ecto/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ defmodule ElixirSense.Plugins.Ecto.Query do

defp infer_type({:__aliases__, _, mods}, _vars, env, buffer_metadata) do
mod = Module.concat(mods)
{actual_mod, _, _} = Util.actual_mod_fun({mod, nil}, false, env, buffer_metadata)
{actual_mod, _, _, _} = Util.actual_mod_fun({mod, nil}, false, env, buffer_metadata)
actual_mod
end

Expand Down
12 changes: 10 additions & 2 deletions lib/elixir_sense/plugins/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ defmodule ElixirSense.Plugins.Util do
end

def actual_mod_fun({mod, fun}, elixir_prefix, env, buffer_metadata) do
%State.Env{imports: imports, requires: requires, aliases: aliases, module: module} = env
%State.Env{
imports: imports,
requires: requires,
aliases: aliases,
module: module,
scope: scope
} = env

%Metadata{mods_funs_to_positions: mods_funs, types: metadata_types} = buffer_metadata

Introspection.actual_mod_fun(
Expand All @@ -48,6 +55,7 @@ defmodule ElixirSense.Plugins.Util do
requires,
if(elixir_prefix, do: [], else: aliases),
module,
scope,
mods_funs,
metadata_types
)
Expand All @@ -66,7 +74,7 @@ defmodule ElixirSense.Plugins.Util do

with %{candidate: {mod, fun}, npar: npar} <- func_info,
mod_fun <- actual_mod_fun({mod, fun}, func_info.elixir_prefix, env, buffer_metadata),
{actual_mod, actual_fun, _} <- mod_fun do
{actual_mod, actual_fun, _, _} <- mod_fun do
{actual_mod, actual_fun, npar, func_info}
else
_ ->
Expand Down
Loading

0 comments on commit a6abc22

Please sign in to comment.