From 8c0c2e918dd63d5b8c5e9823548be19ca32c872c Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 14 Sep 2024 17:12:53 -0400 Subject: [PATCH] improvement: support fragments when finding DSL options fix: support latest elixir_ls changes unfortunately, we're going to have a failing CI until https://github.com/elixir-lsp/elixir_sense/pull/296 is merged --- lib/spark/elixir_sense/aliases.ex | 13 ++-- lib/spark/elixir_sense/behaviour_setter.ex | 11 +++ lib/spark/elixir_sense/entity.ex | 21 ++++-- lib/spark/elixir_sense/plugin.ex | 15 +++- lib/spark/igniter.ex | 81 +++++++++++++++++++++- mix.exs | 7 +- mix.lock | 8 +-- test/igniter_test.exs | 39 +++++++++++ 8 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 lib/spark/elixir_sense/behaviour_setter.ex create mode 100644 test/igniter_test.exs diff --git a/lib/spark/elixir_sense/aliases.ex b/lib/spark/elixir_sense/aliases.ex index 17a57bc..59a7cb4 100644 --- a/lib/spark/elixir_sense/aliases.ex +++ b/lib/spark/elixir_sense/aliases.ex @@ -3,10 +3,15 @@ defmodule Spark.ElixirSense.Types do alias ElixirSense.Core.Introspection - if Code.ensure_loaded?(ElixirLS.LanguageServer.Plugins.Util) do - @util ElixirLS.LanguageServer.Plugins.Util - else - @util ElixirSense.Plugins.Util + cond do + Code.ensure_loaded?(ElixirSense.Providers.Plugins.Util) -> + @util ElixirSense.Providers.Plugins.Util + + Code.ensure_loaded?(ElixirLS.LanguageServer.Plugins.Util) -> + @util ElixirLS.LanguageServer.Plugins.Util + + true -> + @util ElixirSense.Plugins.Util end if Code.ensure_loaded?(ElixirLS.Utils.Matcher) do diff --git a/lib/spark/elixir_sense/behaviour_setter.ex b/lib/spark/elixir_sense/behaviour_setter.ex new file mode 100644 index 0000000..a5a598d --- /dev/null +++ b/lib/spark/elixir_sense/behaviour_setter.ex @@ -0,0 +1,11 @@ +defmodule Spark.ElixirSense.BehaviourSetter do + @moduledoc false + # This module serves as a workaround for changes in ElixirLS behavior implementation. + # It allows us to reference a non-existent behavior without triggering compile-time warnings. + + defmacro __before_compile__(_env) do + quote do + Module.put_attribute(__MODULE__, :behaviour, ElixirLS.LanguageServer.Plugin) + end + end +end diff --git a/lib/spark/elixir_sense/entity.ex b/lib/spark/elixir_sense/entity.ex index 64a3045..081e9e7 100644 --- a/lib/spark/elixir_sense/entity.ex +++ b/lib/spark/elixir_sense/entity.ex @@ -3,10 +3,15 @@ defmodule Spark.ElixirSense.Entity do alias ElixirSense.Core.Introspection alias ElixirSense.Providers.Suggestion.Complete - if Code.ensure_loaded?(ElixirLS.LanguageServer.Plugins.Util) do - @util ElixirLS.LanguageServer.Plugins.Util - else - @util ElixirSense.Plugins.Util + cond do + Code.ensure_loaded?(ElixirSense.Providers.Plugins.Util) -> + @util ElixirSense.Providers.Plugins.Util + + Code.ensure_loaded?(ElixirLS.LanguageServer.Plugins.Util) -> + @util ElixirLS.LanguageServer.Plugins.Util + + true -> + @util ElixirSense.Plugins.Util end def find_entities(type, hint) do @@ -31,6 +36,14 @@ defmodule Spark.ElixirSense.Entity do builtins = if builtins && !String.contains?(hint, ".") && lowercase_string?(hint) do cond do + Code.ensure_loaded?(ElixirSense.Providers.Completion.CompletionEngine) -> + apply(ElixirSense.Providers.Completion.CompletionEngine, :complete, [ + to_string("#{inspect(builtins)}.#{hint}"), + apply(ElixirSense.Core.State.Env, :__struct__, []), + apply(ElixirSense.Core.Metadata, :__struct__, []), + 0 + ]) + Code.ensure_loaded?(ElixirLS.Utils.CompletionEngine) -> apply(ElixirLS.Utils.CompletionEngine, :complete, [ to_string("#{inspect(builtins)}.#{hint}"), diff --git a/lib/spark/elixir_sense/plugin.ex b/lib/spark/elixir_sense/plugin.ex index ae4bbe5..533636e 100644 --- a/lib/spark/elixir_sense/plugin.ex +++ b/lib/spark/elixir_sense/plugin.ex @@ -8,6 +8,8 @@ defmodule Spark.ElixirSense.Plugin do @matcher ElixirSense.Providers.Suggestion.Matcher + @before_compile Spark.ElixirSense.BehaviourSetter + case Code.ensure_compiled(ElixirSense.Plugin) do {:module, _} -> @behaviour ElixirSense.Plugin @@ -18,6 +20,17 @@ defmodule Spark.ElixirSense.Plugin do :ok end + case Code.ensure_compiled(ElixirSense.Providers.Plugin) do + {:module, _} -> + @behaviour ElixirSense.Providers.Plugin + @behaviour ElixirSense.Providers.Completion.GenericReducer + @generic_reducer ElixirSense.Providers.Completion.GenericReducer + @matcher ElixirSense.Providers.Utils.Matcher + + _ -> + :ok + end + case Code.ensure_compiled(ElixirLS.LanguageServer.Plugin) do {:module, _} -> @behaviour ElixirLS.LanguageServer.Plugin @@ -60,7 +73,7 @@ defmodule Spark.ElixirSense.Plugin do suggestions end rescue - _ -> + _e -> :ignore end diff --git a/lib/spark/igniter.ex b/lib/spark/igniter.ex index 619e46d..58fd6d4 100644 --- a/lib/spark/igniter.ex +++ b/lib/spark/igniter.ex @@ -42,16 +42,50 @@ defmodule Spark.Igniter do {:ok, {igniter, _source, zipper}} = Igniter.Code.Module.find_module(igniter, module) - {igniter, do_get_option(zipper, path)} + zipper = + case Igniter.Code.Common.move_to_do_block(zipper) do + {:ok, zipper} -> zipper + _ -> zipper + end + + zipper + |> search_modules(module) + |> Enum.reduce_while({igniter, :error}, fn search_module, {igniter, :error} -> + {igniter, zipper} = + if search_module == module do + {igniter, {:ok, zipper}} + else + with {:ok, {igniter, _source, zipper}} <- + Igniter.Code.Module.find_module(igniter, search_module), + {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do + {igniter, {:ok, zipper}} + else + {:error, igniter} -> + {igniter, :error} + + _ -> + {igniter, :error} + end + end + + with {:ok, zipper} <- zipper, + {:ok, value} <- do_get_option(zipper, path) do + {:halt, {igniter, {:ok, value}}} + else + _ -> {:cont, {igniter, :error}} + end + end) end defp do_get_option(zipper, [{:option, name}]) do with {:ok, zipper} <- Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, name, 1), {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0) do + zipper = Igniter.Code.Common.maybe_move_to_single_child_block(zipper) + case Igniter.Code.Common.expand_literal(zipper) do - {:ok, value} -> value - :error -> zipper.node + {:ok, value} -> {:ok, value} + :error -> {:ok, zipper.node} end else _ -> :error @@ -350,4 +384,45 @@ defmodule Spark.Igniter do end end) end + + # sobelow_skip ["RCE.CodeModule"] + defp search_modules(zipper, base) do + with {:ok, zipper} <- + Igniter.Code.Function.move_to_function_call_in_current_scope( + zipper, + :use, + 2, + fn zipper -> + with {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1), + {:ok, _} <- Igniter.Code.Keyword.get_key(zipper, :fragments) do + true + else + _ -> + false + end + end + ), + {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1), + {:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :fragments) do + evaled = + try do + case Igniter.Code.Common.expand_literal(zipper) do + {:ok, value} -> value + :error -> Code.eval_quoted(zipper.node) + end + rescue + _e -> + [] + end + + if is_list(evaled) do + Enum.uniq([base | Enum.filter(evaled, &is_atom/1)]) + else + [base] + end + else + _ -> + [base] + end + end end diff --git a/mix.exs b/mix.exs index 0f69701..7d1fbcb 100644 --- a/mix.exs +++ b/mix.exs @@ -101,7 +101,7 @@ defmodule Spark.MixProject do {:sourceror, "~> 1.2"}, # in 3.x, make this dependency optional {:jason, "~> 1.4"}, - {:igniter, "~> 0.2 and >= 0.2.6"}, + {:igniter, "~> 0.2 and >= 0.3.36"}, # Dev/Test dependencies {:benchee, "~> 1.3", only: [:dev, :test]}, {:eflame, "~> 1.0", only: [:dev, :test], runtime: false}, @@ -112,7 +112,10 @@ defmodule Spark.MixProject do {:sobelow, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:git_ops, "~> 2.5", only: [:dev, :test]}, {:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false}, - {:elixir_sense, github: "elixir-lsp/elixir_sense", only: [:test, :dev, :docs]}, + {:elixir_sense, + github: "elixir-lsp/elixir_sense", + only: [:test, :docs], + ref: "4adfc55dc6902f56e6d070e14fba768a0ff05bd3"}, {:mix_audit, ">= 0.0.0", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 6b844c1..a7de588 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,7 @@ "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "653ef79ba9b1abab05c576e3406af3def7f5f86e", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "4adfc55dc6902f56e6d070e14fba768a0ff05bd3", [ref: "4adfc55dc6902f56e6d070e14fba768a0ff05bd3"]}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, @@ -14,23 +14,19 @@ "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"}, "glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"}, - "igniter": {:hex, :igniter, "0.3.24", "791a91650ffab9d66b9a3011c66491f767577ad55c363f820cc188554207ee6f", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:owl, "~> 0.9", [hex: :owl, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}, {:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: false]}], "hexpm", "2e1d336534c6129bae0db043fae650303b96974c0488c290191d6d4c61ec9a9f"}, - "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, + "igniter": {:hex, :igniter, "0.3.36", "7dffb41e8c25dac3de8a0947c4973dc3db3cd25f394fd87ac334fe18a725f291", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "5a493222cbf4e3cf0106cd090c93a1f61fa4df958b7d00e03a836e8a67a4bab2"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, - "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "owl": {:hex, :owl, "0.11.0", "2cd46185d330aa2400f1c8c3cddf8d2ff6320baeff23321d1810e58127082cae", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "73f5783f0e963cc04a061be717a0dbb3e49ae0c4bfd55fb4b78ece8d33a65efe"}, "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"}, "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, - "ucwidth": {:hex, :ucwidth, "0.2.0", "1f0a440f541d895dff142275b96355f7e91e15bca525d4a0cc788ea51f0e3441", [:mix], [], "hexpm", "c1efd1798b8eeb11fb2bec3cafa3dd9c0c3647bee020543f0340b996177355bf"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, } diff --git a/test/igniter_test.exs b/test/igniter_test.exs new file mode 100644 index 0000000..d476671 --- /dev/null +++ b/test/igniter_test.exs @@ -0,0 +1,39 @@ +defmodule Spark.IgniterTest do + use ExUnit.Case + + import Igniter.Test + + test "options are found in DSLs" do + assert {_igniter, {:ok, Bar.Baz}} = + test_project() + |> Igniter.Code.Module.create_module(TedDansen, """ + use Spark.Test.Contact + + contact do + module(Bar.Baz) + end + """) + |> Spark.Igniter.get_option(TedDansen, [:contact, :module]) + end + + test "options are found in fragments" do + assert {_igniter, {:ok, "foobar"}} = + test_project() + |> Igniter.Code.Module.create_module(TedDansenFragment, """ + @moduledoc false + use Spark.Dsl.Fragment, of: Spark.Test.Contact + + address do + street("foobar") + end + """) + |> Igniter.Code.Module.create_module(TedDansen, """ + use Spark.Test.Contact, fragments: [TedDansenFragment] + + contact do + module(Bar.Baz) + end + """) + |> Spark.Igniter.get_option(TedDansen, [:address, :street]) + end +end