Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: module attributes #215

Merged
merged 5 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@ defmodule NextLS do
[module, "alias"]
)

{:attribute, module, attribute} ->
DB.query(
database,
~Q"""
SELECT file, start_line, end_line, start_column, end_column
FROM "references" as refs
WHERE refs.identifier = ?
AND refs.type = ?
AND refs.module = ?
AND refs.source = 'user'
""",
[attribute, "attribute", module]
)

:unknown ->
[]
end
Expand Down Expand Up @@ -266,7 +280,7 @@ defmodule NextLS do
filtered_symbols =
for {pid, _} <- entries, symbol <- symbols.(pid), score = fuzzy_match(symbol.name, query, case_sensitive?) do
name =
if symbol.type != "defstruct" do
if symbol.type not in ["defstruct", "attribute"] do
"#{symbol.type} #{symbol.name}"
else
"#{symbol.name}"
Expand Down Expand Up @@ -679,6 +693,7 @@ defmodule NextLS do

defp elixir_kind_to_lsp_kind("defmodule"), do: GenLSP.Enumerations.SymbolKind.module()
defp elixir_kind_to_lsp_kind("defstruct"), do: GenLSP.Enumerations.SymbolKind.struct()
defp elixir_kind_to_lsp_kind("attribute"), do: GenLSP.Enumerations.SymbolKind.property()

defp elixir_kind_to_lsp_kind(kind) when kind in ["def", "defp", "defmacro", "defmacrop"],
do: GenLSP.Enumerations.SymbolKind.function()
Expand Down Expand Up @@ -737,11 +752,17 @@ defmodule NextLS do
[[module, "defmacro", function]] ->
{:function, module, function}

[[module, "attribute", attribute]] ->
{:attribute, module, attribute}

_unknown_definition ->
case DB.query(database, reference_query, [file, line, col]) do
[[function, "function", module]] ->
{:function, module, function}

[[attribute, "attribute", module]] ->
{:attribute, module, attribute}

[[_alias, "alias", module]] ->
{:module, module}

Expand Down
75 changes: 75 additions & 0 deletions lib/next_ls/ast_helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule NextLS.ASTHelpers do
@moduledoc false

@spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() | nil
def get_attribute_reference_name(file, line, column) do
ast = ast_from_file(file)

{_ast, name} =
Macro.prewalk(ast, nil, fn
{:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, "@#{name}"}
other, acc -> {other, acc}
end)

name
end

@spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}]
def get_module_attributes(file, module) do
reserved_attributes = Module.reserved_attributes()

symbols = parse_symbols(file, module)

Enum.filter(symbols, fn
{:attribute, "@" <> name, _, _} ->
not Map.has_key?(reserved_attributes, String.to_atom(name))

_other ->
false
end)
end

defp parse_symbols(file, module) do
ast = ast_from_file(file)

{_ast, %{symbols: symbols}} =
Macro.traverse(ast, %{modules: [], symbols: []}, &prewalk/2, &postwalk(&1, &2, module))

symbols
end

# add module name to modules stack on enter
defp prewalk({:defmodule, _, [{:__aliases__, _, module_name_atoms} | _]} = ast, acc) do
modules = [module_name_atoms | acc.modules]
{ast, %{acc | modules: modules}}
end

defp prewalk(ast, acc), do: {ast, acc}

defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do
ast_module =
acc.modules
|> Enum.reverse()
|> List.flatten()
|> Module.concat()

if module == ast_module do
symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols]
{ast, %{acc | symbols: symbols}}
else
{ast, acc}
end
end

# remove module name from modules stack on exit
defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do
[_exit_mudule | modules] = acc.modules
{ast, %{acc | modules: modules}}
end

defp postwalk(ast, acc, _module), do: {ast, acc}

defp ast_from_file(file) do
file |> File.read!() |> Code.string_to_quoted!(columns: true)
end
end
12 changes: 12 additions & 0 deletions lib/next_ls/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ defmodule NextLS.DB do
struct: struct,
file: file,
defs: defs,
symbols: symbols,
source: source
} = symbol

Expand Down Expand Up @@ -106,6 +107,17 @@ defmodule NextLS.DB do
)
end

for {type, name, line, column} <- symbols do
__query__(
{conn, s.logger},
~Q"""
INSERT INTO symbols (module, file, type, name, line, 'column', source)
VALUES (?, ?, ?, ?, ?, ?, ?);
""",
[mod, file, type, name, line, column, source]
)
end

{:noreply, s}
end

Expand Down
3 changes: 3 additions & 0 deletions lib/next_ls/definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ defmodule NextLS.Definition do
"function" ->
[module, identifier]

"attribute" ->
[module, identifier]

_ ->
nil
end
Expand Down
10 changes: 10 additions & 0 deletions lib/next_ls/runtime/sidecar.ex
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be helpful to extra a module for single use AST functions, to get them out of this sidecar process, as it's just meant to be a sidecar/proxy.

You can then unit test them if you'd like (I don't think it's necessary for this PR, but as we discover more complicated edge cases, would be useful)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's good idea. I extracted it. I'm not happy about module design but I think it could be refactored later with more usecases.
I'm not sure about naming so if you like call it differently I don't mind

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule NextLS.Runtime.Sidecar do
@moduledoc false
use GenServer

alias NextLS.ASTHelpers
alias NextLS.DB

def start_link(args) do
Expand All @@ -15,11 +16,20 @@ defmodule NextLS.Runtime.Sidecar do
end

def handle_info({:tracer, payload}, state) do
attributes = ASTHelpers.get_module_attributes(payload.file, payload.module)
payload = Map.put_new(payload, :symbols, attributes)
DB.insert_symbol(state.db, payload)

{:noreply, state}
end

def handle_info({{:tracer, :reference, :attribute}, payload}, state) do
name = ASTHelpers.get_attribute_reference_name(payload.file, payload.meta[:line], payload.meta[:column])
if name, do: DB.insert_reference(state.db, %{payload | identifier: name})

{:noreply, state}
end

def handle_info({{:tracer, :reference}, payload}, state) do
DB.insert_reference(state.db, payload)

Expand Down
21 changes: 21 additions & 0 deletions priv/monkey/_next_ls_private_compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ defmodule NextLSPrivate.Tracer do
:ok
end

def trace({:imported_macro, meta, _module, :@, arity}, env) do
parent = parent_pid()

Process.send(
parent,
{{:tracer, :reference, :attribute},
%{
meta: meta,
identifier: :@,
arity: arity,
file: env.file,
type: :attribute,
module: env.module,
source: @source
}},
[]
)

:ok
end

def trace({type, meta, module, func, arity}, env) when type in [:remote_function, :remote_macro, :imported_macro] do
parent = parent_pid()

Expand Down
155 changes: 155 additions & 0 deletions test/next_ls/definition_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,159 @@ defmodule NextLS.DefinitionTest do
500
end
end

describe "attribute" do
@describetag root_paths: ["my_proj"]
setup %{tmp_dir: tmp_dir} do
File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib"))
File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs())
[cwd: tmp_dir]
end

setup %{cwd: cwd} do
bar = Path.join(cwd, "my_proj/lib/bar.ex")

File.write!(bar, """
defmodule Bar do
@my_attr 1
@second_attr 2

@spec run() :: :ok | :error
def run() do
if @my_attr == 1 do
:ok
else
{:error, @second_attr}
end
end

defmodule Inner do
@inner_attr 123

def foo(a) do
if a, do: @inner_attr
end
end

def foo() do
:nothing
end
end

defmodule TopSecond.Some.Long.Name do
@top_second_attr "something"

def run_second do
{:error, @top_second_attr}
end
end
""")

[bar: bar]
end

setup :with_lsp

test "go to attribute definition", %{client: client, bar: bar} do
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
assert_request(client, "client/registerCapability", fn _params -> nil end)
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}

uri = uri(bar)

request(client, %{
method: "textDocument/definition",
id: 4,
jsonrpc: "2.0",
params: %{
position: %{line: 6, character: 9},
textDocument: %{uri: uri}
}
})

assert_result 4,
%{
"range" => %{
"start" => %{
"line" => 1,
"character" => 2
},
"end" => %{
"line" => 1,
"character" => 2
}
},
"uri" => ^uri
},
500
end

test "go to attribute definition in second module", %{client: client, bar: bar} do
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
assert_request(client, "client/registerCapability", fn _params -> nil end)
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}

uri = uri(bar)

request(client, %{
method: "textDocument/definition",
id: 4,
jsonrpc: "2.0",
params: %{
position: %{line: 30, character: 17},
textDocument: %{uri: uri}
}
})

assert_result 4,
%{
"range" => %{
"start" => %{
"line" => 27,
"character" => 2
},
"end" => %{
"line" => 27,
"character" => 2
}
},
"uri" => ^uri
},
500
end

test "go to attribute definition in inner module", %{client: client, bar: bar} do
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
assert_request(client, "client/registerCapability", fn _params -> nil end)
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}

uri = uri(bar)

request(client, %{
method: "textDocument/definition",
id: 4,
jsonrpc: "2.0",
params: %{
position: %{line: 17, character: 20},
textDocument: %{uri: uri}
}
})

assert_result 4,
%{
"range" => %{
"start" => %{
"line" => 14,
"character" => 4
},
"end" => %{
"line" => 14,
"character" => 4
}
},
"uri" => ^uri
},
500
end
end
end
Loading
Loading