Skip to content

Commit 70b128c

Browse files
authored
Merge pull request #1267 from elixir-lsp/toxic2-integration
Integrate the toxic2 tolerant parser across providers
2 parents 16bbcdf + c0f7245 commit 70b128c

27 files changed

Lines changed: 1050 additions & 1965 deletions

File tree

apps/elixir_ls_utils/lib/completion_engine.ex

Lines changed: 137 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ defmodule ElixirLS.Utils.CompletionEngine do
9797
%{type: :bitstring_option, name: "utf32"}
9898
]
9999

100+
# Bitstring modifier validity rules (UTS of `<<x::type-sign-endianness-size-unit>>`). Used to keep
101+
# `:bitstring_modifier` completion from offering combinations Elixir rejects (e.g. `signed` after
102+
# `binary-`, `size`/`unit` after `utf8-`). Inlined here rather than pulled from a separate module so
103+
# the completion engine stays self-contained.
104+
@bitstring_types [:integer, :float, :bitstring, :binary, :utf8, :utf16, :utf32]
105+
@bitstring_utf_types [:utf8, :utf16, :utf32]
106+
@bitstring_type_aliases [bits: :bitstring, bytes: :binary]
107+
@bitstring_sign_modifiers [:signed, :unsigned]
108+
@bitstring_endianness_modifiers [:little, :big, :native]
109+
# types each endianness modifier is compatible with, when the type is not yet fixed
110+
@bitstring_endianness_types [:integer, :float, :utf16, :utf32]
111+
@bitstring_default_state %{
112+
type: nil,
113+
sign_modifier: nil,
114+
endianness_modifier: nil,
115+
size: nil,
116+
unit: nil
117+
}
118+
100119
@alias_only_atoms ~w(alias import require)a
101120
@alias_only_charlists ~w(alias import require)c
102121

@@ -722,18 +741,37 @@ defmodule ElixirLS.Utils.CompletionEngine do
722741
end
723742

724743
defp struct_module_filter(true, %State.Env{} = _env, %Metadata{} = metadata) do
744+
# Build the loaded/application module-name list ONCE for the whole filter pass
745+
# rather than recomputing `:code.all_loaded()` (and the application module scan)
746+
# for every candidate module - that turned the filter into O(N^2) work.
747+
all_module_names = all_module_names()
748+
725749
fn module ->
726750
Struct.is_struct(module, metadata.structs) or
727-
has_struct_submodule?(module, metadata.structs)
751+
has_struct_submodule?(module, metadata.structs, all_module_names)
728752
end
729753
end
730754

731755
defp struct_module_filter(false, %State.Env{} = _env, %Metadata{} = _metadata) do
732756
fn _ -> true end
733757
end
734758

759+
# The loaded (and, in interactive mode, application) module names as strings.
760+
# Computed once per struct-module filter pass, not per candidate module.
761+
defp all_module_names do
762+
modules = Enum.map(:code.all_loaded(), &Atom.to_string(elem(&1, 0)))
763+
764+
case :code.get_mode() do
765+
:interactive ->
766+
modules ++ Enum.map(Applications.get_modules_from_applications(), &Atom.to_string/1)
767+
768+
_ ->
769+
modules
770+
end
771+
end
772+
735773
# Check if a module has any direct submodules that are structs
736-
defp has_struct_submodule?(module, structs) do
774+
defp has_struct_submodule?(module, structs, all_module_names) do
737775
module_str = Atom.to_string(module)
738776

739777
# Check metadata structs (from current buffer)
@@ -747,30 +785,14 @@ defmodule ElixirLS.Utils.CompletionEngine do
747785
if metadata_result do
748786
true
749787
else
750-
# Get all modules and check if any direct submodule is a struct
788+
# Check if any direct submodule (from the precomputed list) is a struct
751789
module_str_with_dot = module_str <> "."
752790

753-
# Get all loaded modules
754-
modules = Enum.map(:code.all_loaded(), &Atom.to_string(elem(&1, 0)))
755-
756-
# Add modules from applications if in interactive mode
757-
modules =
758-
case :code.get_mode() do
759-
:interactive ->
760-
modules ++
761-
Enum.map(Applications.get_modules_from_applications(), &Atom.to_string/1)
762-
763-
_ ->
764-
modules
765-
end
766-
767-
# Find submodules
768791
submodules =
769-
for mod <- modules,
792+
for mod <- all_module_names,
770793
String.starts_with?(mod, module_str_with_dot),
771794
do: String.to_atom(mod)
772795

773-
# Check if any submodule is a struct
774796
Enum.any?(submodules, fn mod ->
775797
Code.ensure_loaded?(mod) and function_exported?(mod, :__struct__, 1)
776798
end)
@@ -793,16 +815,22 @@ defmodule ElixirLS.Utils.CompletionEngine do
793815
{container_context_map_fields(pairs, {:struct, alias}, map, hint, metadata), continue?}
794816

795817
:bitstring_modifier ->
796-
existing =
818+
# Parse the modifiers already typed after `::` into a state and keep only the modifiers that
819+
# may still legally follow (honoring the type / sign / endianness / utf / size / unit rules),
820+
# instead of merely excluding names already present - which would offer invalid combinations
821+
# such as `signed`/`little` after `binary-`, or `size`/`unit`/`signed` after `utf8-`.
822+
available_names =
797823
code
798824
|> List.to_string()
799825
|> String.split("::")
800826
|> List.last()
801-
|> String.split("-")
827+
|> bitstring_parse()
828+
|> bitstring_available_options()
829+
|> Enum.map(&Atom.to_string/1)
802830

803831
results =
804832
@bitstring_modifiers
805-
|> Enum.filter(&(Matcher.match?(&1.name, hint) and &1.name not in existing))
833+
|> Enum.filter(&(&1.name in available_names and Matcher.match?(&1.name, hint)))
806834
|> format_expansion()
807835

808836
{results, false}
@@ -812,6 +840,92 @@ defmodule ElixirLS.Utils.CompletionEngine do
812840
end
813841
end
814842

843+
# Parse the modifier text already typed after `::` (e.g. `binary-siz`) into a
844+
# `%{type, sign_modifier, endianness_modifier, size, unit}` state. Unknown/partial characters (the
845+
# in-progress hint) are skipped one at a time, so a complete parse is not required.
846+
defp bitstring_parse(text), do: bitstring_parse(text, @bitstring_default_state)
847+
848+
for type <- @bitstring_types do
849+
defp bitstring_parse(<<unquote(Atom.to_string(type)), rest::binary>>, acc),
850+
do: bitstring_parse(rest, %{acc | type: unquote(type)})
851+
end
852+
853+
for {type_alias, type} <- @bitstring_type_aliases do
854+
defp bitstring_parse(<<unquote(Atom.to_string(type_alias)), rest::binary>>, acc),
855+
do: bitstring_parse(rest, %{acc | type: unquote(type)})
856+
end
857+
858+
for sign <- @bitstring_sign_modifiers do
859+
defp bitstring_parse(<<unquote(Atom.to_string(sign)), rest::binary>>, acc),
860+
do: bitstring_parse(rest, %{acc | sign_modifier: unquote(sign)})
861+
end
862+
863+
for endianness <- @bitstring_endianness_modifiers do
864+
defp bitstring_parse(<<unquote(Atom.to_string(endianness)), rest::binary>>, acc),
865+
do: bitstring_parse(rest, %{acc | endianness_modifier: unquote(endianness)})
866+
end
867+
868+
defp bitstring_parse(<<"-", rest::binary>>, acc), do: bitstring_parse(rest, acc)
869+
870+
defp bitstring_parse(<<"size", rest::binary>>, acc),
871+
do: bitstring_parse(rest, %{acc | size: true})
872+
873+
defp bitstring_parse(<<"unit", rest::binary>>, acc),
874+
do: bitstring_parse(rest, %{acc | unit: true})
875+
876+
defp bitstring_parse(<<_::binary-size(1), rest::binary>>, acc), do: bitstring_parse(rest, acc)
877+
defp bitstring_parse(<<>>, acc), do: acc
878+
879+
# The modifiers that may still legally follow, given the parsed state.
880+
defp bitstring_available_options(state) do
881+
bitstring_available_types(state) ++
882+
bitstring_available_sign_modifiers(state) ++
883+
bitstring_available_endianness_modifiers(state) ++
884+
bitstring_available_size(state) ++
885+
bitstring_available_unit(state)
886+
end
887+
888+
defp bitstring_available_types(
889+
%{type: nil, sign_modifier: nil, endianness_modifier: nil} = state
890+
),
891+
do: bitstring_filter_utf(state, @bitstring_types)
892+
893+
defp bitstring_available_types(%{type: nil, sign_modifier: nil, endianness_modifier: e} = state)
894+
when not is_nil(e),
895+
do: bitstring_filter_utf(state, @bitstring_endianness_types)
896+
897+
defp bitstring_available_types(%{type: nil}), do: [:integer]
898+
defp bitstring_available_types(_), do: []
899+
900+
defp bitstring_available_sign_modifiers(%{type: type, sign_modifier: nil})
901+
when type in [nil, :integer],
902+
do: @bitstring_sign_modifiers
903+
904+
defp bitstring_available_sign_modifiers(_), do: []
905+
906+
defp bitstring_available_endianness_modifiers(%{type: type, endianness_modifier: nil})
907+
when type in [nil, :integer, :utf16, :utf32],
908+
do: @bitstring_endianness_modifiers
909+
910+
defp bitstring_available_endianness_modifiers(%{type: :float, endianness_modifier: nil}),
911+
do: [:little, :big]
912+
913+
defp bitstring_available_endianness_modifiers(_), do: []
914+
915+
# size and unit are unsupported on utf types (they fail to compile).
916+
defp bitstring_available_size(%{size: nil, type: type}) when type not in @bitstring_utf_types,
917+
do: [:size]
918+
919+
defp bitstring_available_size(_), do: []
920+
921+
defp bitstring_available_unit(%{unit: nil, type: type}) when type not in @bitstring_utf_types,
922+
do: [:unit]
923+
924+
defp bitstring_available_unit(_), do: []
925+
926+
defp bitstring_filter_utf(%{size: nil, unit: nil}, list), do: list
927+
defp bitstring_filter_utf(_, list), do: list -- @bitstring_utf_types
928+
815929
defp container_context_map_fields(pairs, kind, map, hint, metadata) do
816930
{keys, types, alias, doc, meta} =
817931
case kind do

apps/elixir_ls_utils/test/complete_test.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,6 +2033,32 @@ defmodule ElixirLS.Utils.CompletionEngineTest do
20332033
refute Enum.any?(entries, &(&1.name == "integer"))
20342034
refute Enum.any?(entries, &(&1.name == "little"))
20352035
assert Enum.any?(entries, &(&1.name == "size" and &1.arity == 1))
2036+
2037+
# state-based validity: a `binary` type only allows size/unit to follow
2038+
names = fn code ->
2039+
expand(code) |> Enum.filter(&(&1[:type] == :bitstring_option)) |> Enum.map(& &1.name)
2040+
end
2041+
2042+
binary = names.('<<foo::binary-')
2043+
assert "size" in binary
2044+
assert "unit" in binary
2045+
refute "signed" in binary
2046+
refute "little" in binary
2047+
refute "native" in binary
2048+
refute "utf8" in binary
2049+
2050+
# utf types take no further modifiers
2051+
assert names.('<<foo::utf8-') == []
2052+
2053+
# float allows little/big (not native), size and unit - but not sign or another type
2054+
float = names.('<<foo::float-')
2055+
assert "little" in float
2056+
assert "big" in float
2057+
assert "size" in float
2058+
assert "unit" in float
2059+
refute "native" in float
2060+
refute "signed" in float
2061+
refute "binary" in float
20362062
end
20372063

20382064
test "completion for aliases in special forms" do

0 commit comments

Comments
 (0)