From ac81dcb83f0e0fdf8e976d06020bcc0aeda72f40 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 14 Aug 2024 00:23:35 +0200 Subject: [PATCH] expand before_compile --- lib/elixir_sense/core/compiler.ex | 28 ++++++- lib/elixir_sense/core/state.ex | 74 +++++++++++++++---- .../core/metadata_builder_test.exs | 53 ++++++++++--- 3 files changed, 127 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 8c797c4a..d928e1ef 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1163,7 +1163,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_attribute(name, inferred_type, is_definition, meta) + |> add_attribute(env, name, meta, e_args, inferred_type, is_definition) |> add_current_env_to_line(meta, env) {{:@, meta, [{name, name_meta, e_args}]}, state, env} @@ -1475,7 +1475,26 @@ defmodule ElixirSense.Core.Compiler do {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) - {result, state, _env} = expand(block, state, %{env | module: full}) + {result, state, e_env} = expand(block, state, %{env | module: full}) + + before_compile = + for args <- Map.get(state.attribute_store, {full, :before_compile}, []) do + target = + case args do + {module, fun} -> [module, fun] + module -> [module, :__before_compile__] + end + + {:__block__, [], + [ + {:require, [], [hd(target)]}, + {{:., [], target}, [], [e_env]} + ]} + end + + module_callbacks = {:__block__, [], before_compile} + + {_result, state, _e_env} = expand(module_callbacks, state, e_env) state = state @@ -1504,8 +1523,9 @@ defmodule ElixirSense.Core.Compiler do |> remove_attributes_scope |> remove_module - # TODO hardcode expansion? - # to result of require (a module atom) and :elixir_module.compile dot call in block + # in elixir the result of defmodule expansion is + # require (a module atom) and :elixir_module.compile dot call in block + # we don't need that {{:__block__, [], []}, state, env} end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 492c911e..6bfd8ffb 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -66,7 +66,8 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: list(), lines_to_env: lines_to_env_t, cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()}, - ex_unit_describe: nil | atom + ex_unit_describe: nil | atom, + attribute_store: %{optional(module) => term} } defstruct attributes: [[]], @@ -98,7 +99,8 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: [[]], lines_to_env: %{}, cursor_env: nil, - ex_unit_describe: nil + ex_unit_describe: nil, + attribute_store: %{} defmodule Env do @moduledoc """ @@ -515,8 +517,7 @@ defmodule ElixirSense.Core.State do doc, meta, options - ) - when is_tuple(position) do + ) do current_info = Map.get(state.mods_funs_to_positions, {module, fun, arity}, %ModFunInfo{}) current_params = current_info |> Map.get(:params, []) current_positions = current_info |> Map.get(:positions, []) @@ -980,10 +981,7 @@ defmodule ElixirSense.Core.State do def add_var_read(%__MODULE__{} = state, _), do: state - @builtin_attributes ElixirSense.Core.BuiltinAttributes.all() - - def add_attribute(%__MODULE__{} = state, attribute, type, is_definition, meta) - when attribute not in @builtin_attributes do + def add_attribute(%__MODULE__{} = state, env, attribute, meta, args, type, is_definition) do position = extract_position(meta) [attributes_from_scope | other_attributes] = state.attributes @@ -1023,11 +1021,47 @@ defmodule ElixirSense.Core.State do attributes = [attributes_from_scope | other_attributes] scope_attributes = [attributes_from_scope | tl(state.scope_attributes)] - %__MODULE__{state | attributes: attributes, scope_attributes: scope_attributes} - end - def add_attribute(%__MODULE__{} = state, _attribute, _type, _is_definition, _meta) do - state + # TODO handle other + # {moduledoc, nil, nil, []}, + # {after_compile, [], accumulate, []}, + # {after_verify, [], accumulate, []}, + # {before_compile, [], accumulate, []}, + # {behaviour, [], accumulate, []}, + # {compile, [], accumulate, []}, + # {derive, [], accumulate, []}, + # {dialyzer, [], accumulate, []}, + # {external_resource, [], accumulate, []}, + # {on_definition, [], accumulate, []}, + # {type, [], accumulate, []}, + # {opaque, [], accumulate, []}, + # {typep, [], accumulate, []}, + # {spec, [], accumulate, []}, + # {callback, [], accumulate, []}, + # {macrocallback, [], accumulate, []}, + # {optional_callbacks, [], accumulate, []}, + accumulating? = + attribute in [:before_compile, :after_compile, :after_verify, :on_definition, :on_load] + + attribute_store = + if is_definition do + [arg] = args + + if accumulating? do + state.attribute_store |> Map.update({env.module, attribute}, [arg], &(&1 ++ [arg])) + else + state.attribute_store |> Map.put({env.module, attribute}, arg) + end + else + state.attribute_store + end + + %__MODULE__{ + state + | attributes: attributes, + scope_attributes: scope_attributes, + attribute_store: attribute_store + } end def add_behaviour(module, %__MODULE__{} = state, env) when is_atom(module) do @@ -1173,8 +1207,13 @@ defmodule ElixirSense.Core.State do ] def add_module_functions(state, env, functions, range) do - {{line, column}, _} = range - meta = [line: line || 0] ++ if(column > 0, do: [column: column], else: []) + {line, column} = + case range do + {{line, column}, _} -> {line, column} + _ -> {0, nil} + end + + meta = [line: line] ++ if(column > 0, do: [column: column], else: []) (functions ++ @module_functions) |> Enum.reduce(state, fn {name, args, kind}, acc -> @@ -1197,7 +1236,12 @@ defmodule ElixirSense.Core.State do end def add_struct_or_exception(state, env, type, fields, range) do - {{line, column}, _} = range + {line, column} = + case range do + {{line, column}, _} -> {line, column} + _ -> {0, nil} + end + meta = [line: line || 0] ++ if(column > 0, do: [column: column], else: []) fields = diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 99ffe147..725d48b0 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -4245,26 +4245,34 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defmacro __using__(_options) do quote do alias Some.First - IO.inspect({__ENV__.aliases, __ENV__.macro_aliases}) end end end - test "macro alias" do + test "macro alias does not leak outside macro" do state = """ defmodule MyModule do use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Definer use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Aliaser IO.puts "" - a = %MyModule.First{} - b = %MyModule.Second{} end """ |> string_to_state assert [{First, {_, Some.First}}] = state.lines_to_env[4].macro_aliases - # TODO should we handle @before_compile? + + assert %{ + MyModule.First => %StructInfo{ + fields: [foo: :bar, __struct__: MyModule.First] + }, + MyModule.Second => %StructInfo{ + fields: [ + baz: {:%, [], [MyModule.First, {:%{}, [], [{:foo, :bar}]}]}, + __struct__: MyModule.Second + ] + } + } = state.structs end end @@ -5434,7 +5442,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do use Application @behaviour SomeModule.SomeBehaviour IO.puts "" - defmodule InnerModuleWithUse do + defmodule InnerModuleWithUse1 do use GenServer IO.puts "" end @@ -5997,6 +6005,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ]) assert [ + %AttributeInfo{name: :before_compile, positions: [{2, _}]}, %AttributeInfo{name: :my_attribute, positions: [{2, _}]} ] = get_line_attributes(state, 4) @@ -7294,7 +7303,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "registers calls on ex_unit DSL" do state = """ - defmodule MyModuleTest do + defmodule MyModuleTests do use ExUnit.Case describe "describe1" do @@ -7664,7 +7673,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "gets ExUnit imports from `use ExUnit.Case`" do state = """ - defmodule MyTest do + defmodule MyModuleTest do use ExUnit.Case IO.puts "" end @@ -7678,7 +7687,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "gets ExUnit imports from case template" do state = """ - defmodule MyTest do + defmodule My1Test do use ElixirSenseExample.CaseTemplateExample IO.puts "" end @@ -8421,6 +8430,32 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state end + describe "module callbacks" do + defmodule Callbacks do + defmacro __before_compile__(_arg) do + quote do + def constant, do: 1 + defoverridable constant: 0 + end + end + end + + test "before_compile" do + state = + """ + defmodule User do + @before_compile ElixirSense.Core.MetadataBuilderTest.Callbacks + end + """ + |> string_to_state + + assert %ModFunInfo{meta: %{overridable: true}} = + state.mods_funs_to_positions[{User, :constant, 0}] + end + + # TODO after_compile?, after_verify?, on_defined, on_load? + end + defp string_to_state(string) do string |> Code.string_to_quoted(columns: true, token_metadata: true)