Skip to content

feat(extension): credo #163

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

Merged
merged 5 commits into from
Aug 17, 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
28 changes: 22 additions & 6 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ defmodule NextLS do
dynamic_supervisor = Keyword.fetch!(args, :dynamic_supervisor)

registry = Keyword.fetch!(args, :registry)
extensions = Keyword.get(args, :extensions, [NextLS.ElixirExtension])

extensions = Keyword.get(args, :extensions, [NextLS.ElixirExtension, NextLS.CredoExtension])
cache = Keyword.fetch!(args, :cache)
{:ok, logger} = DynamicSupervisor.start_child(dynamic_supervisor, {NextLS.Logger, lsp: lsp})

Expand Down Expand Up @@ -363,7 +364,11 @@ defmodule NextLS do
{:ok, _} =
DynamicSupervisor.start_child(
lsp.assigns.dynamic_supervisor,
{extension, cache: lsp.assigns.cache, registry: lsp.assigns.registry, publisher: self()}
{extension,
cache: lsp.assigns.cache,
registry: lsp.assigns.registry,
publisher: self(),
task_supervisor: lsp.assigns.runtime_task_supervisor}
)
end

Expand Down Expand Up @@ -411,7 +416,14 @@ defmodule NextLS do
if status == :ready do
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
send(parent, {:runtime_ready, name, self()})

msg = {:runtime_ready, name, self()}

dispatch(lsp.assigns.registry, :extensions, fn entries ->
for {pid, _} <- entries, do: send(pid, msg)
end)

send(parent, msg)
else
Progress.stop(lsp, token)
GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize")
Expand Down Expand Up @@ -526,7 +538,13 @@ defmodule NextLS do
if status == :ready do
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
send(parent, {:runtime_ready, name, self()})
msg = {:runtime_ready, name, self()}

dispatch(lsp.assigns.registry, :extensions, fn entries ->
for {pid, _} <- entries, do: send(pid, msg)
end)

send(parent, msg)
else
Progress.stop(lsp, token)
GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize")
Expand Down Expand Up @@ -596,8 +614,6 @@ defmodule NextLS do
end

def handle_info(:publish, lsp) do
GenLSP.log(lsp, "[NextLS] Compiled!")

all =
for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), {file, diagnostics} <- cache, reduce: %{} do
d -> Map.update(d, file, diagnostics, fn value -> value ++ diagnostics end)
Expand Down
6 changes: 6 additions & 0 deletions lib/next_ls/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ defmodule NextLS.DB do
is_atom(arg) and String.starts_with?(to_string(arg), "Elixir.") ->
Macro.to_string(arg)

arg in [nil, :undefined] ->
arg

is_atom(arg) ->
to_string(arg)

true ->
arg
end
Expand Down
160 changes: 160 additions & 0 deletions lib/next_ls/extensions/credo_extension.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
defmodule NextLS.CredoExtension do
@moduledoc false
use GenServer

alias GenLSP.Enumerations.DiagnosticSeverity
alias GenLSP.Structures.CodeDescription
alias GenLSP.Structures.Diagnostic
alias GenLSP.Structures.Position
alias GenLSP.Structures.Range
alias NextLS.DiagnosticCache
alias NextLS.Runtime

def start_link(args) do
GenServer.start_link(
__MODULE__,
Keyword.take(args, [:cache, :registry, :publisher, :task_supervisor]),
Keyword.take(args, [:name])
)
end

@impl GenServer
def init(args) do
cache = Keyword.fetch!(args, :cache)
registry = Keyword.fetch!(args, :registry)
publisher = Keyword.fetch!(args, :publisher)
task_supervisor = Keyword.fetch!(args, :task_supervisor)

Registry.register(registry, :extensions, :credo)

{:ok,
%{
runtimes: Map.new(),
cache: cache,
registry: registry,
task_supervisor: task_supervisor,
publisher: publisher,
refresh_refs: Map.new()
}}
end

@impl GenServer

def handle_info({:runtime_ready, _, _}, state), do: {:noreply, state}

def handle_info({:compiler, _diagnostics}, state) do
{state, refresh_refs} =
dispatch(state.registry, :runtimes, fn entries ->
# loop over runtimes
for {runtime, %{path: path}} <- entries, reduce: {state, %{}} do
{state, refs} ->
# determine the existence of Credo and memoize the result
state =
if not Map.has_key?(state.runtimes, runtime) do
case Runtime.call(runtime, {Code, :ensure_loaded?, [Credo]}) do
{:ok, true} ->
:next_ls
|> :code.priv_dir()
|> Path.join("monkey/_next_ls_private_credo.ex")
|> then(&Runtime.call(runtime, {Code, :compile_file, [&1]}))

Runtime.call(runtime, {Application, :ensure_all_started, [:credo]})
Runtime.call(runtime, {GenServer, :call, [Credo.CLI.Output.Shell, {:suppress_output, true}]})

put_in(state.runtimes[runtime], true)

_ ->
state
end
else
state
end

# if runtime has Credo
if state.runtimes[runtime] do
namespace = {:credo, path}
DiagnosticCache.clear(state.cache, namespace)

task =
Task.Supervisor.async_nolink(state.task_supervisor, fn ->
case Runtime.call(runtime, {:_next_ls_private_credo, :issues, [path]}) do
{:ok, issues} -> issues
_error -> []
end
end)

{state, Map.put(refs, task.ref, namespace)}
else
{state, refs}
end
end
end)

send(state.publisher, :publish)

{:noreply, put_in(state.refresh_refs, refresh_refs)}
end

def handle_info({ref, issues}, %{refresh_refs: refs} = state) when is_map_key(refs, ref) do
Process.demonitor(ref, [:flush])
{{:credo, path} = namespace, refs} = Map.pop(refs, ref)

for issue <- issues do
diagnostic = %Diagnostic{
range: %Range{
start: %Position{
line: issue.line_no - 1,
character: (issue.column || 1) - 1
},
end: %Position{
line: issue.line_no - 1,
character: 999
}
},
severity: category_to_severity(issue.category),
data: %{check: issue.check, file: issue.filename},
source: "credo",
code: Macro.to_string(issue.check),
code_description: %CodeDescription{
href: "https://hexdocs.pm/credo/#{Macro.to_string(issue.check)}.html"
},
message: issue.message
}

DiagnosticCache.put(state.cache, namespace, Path.join(path, issue.filename), diagnostic)
end

send(state.publisher, :publish)

{:noreply, put_in(state.refresh_refs, refs)}
end

def handle_info({:DOWN, ref, :process, _pid, _reason}, %{refresh_refs: refs} = state) when is_map_key(refs, ref) do
{_, refs} = Map.pop(refs, ref)

{:noreply, put_in(state.refresh_refs, refs)}
end

defp dispatch(registry, key, callback) do
ref = make_ref()
me = self()

Registry.dispatch(registry, key, fn entries ->
result = callback.(entries)

send(me, {ref, result})
end)

receive do
{^ref, result} -> result
end
end

defp category_to_severity(:refactor), do: DiagnosticSeverity.error()
defp category_to_severity(:warning), do: DiagnosticSeverity.warning()
defp category_to_severity(:design), do: DiagnosticSeverity.information()

defp category_to_severity(:consistency), do: DiagnosticSeverity.information()

defp category_to_severity(:readability), do: DiagnosticSeverity.information()
end
4 changes: 4 additions & 0 deletions lib/next_ls/extensions/elixir_extension.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ defmodule NextLS.ElixirExtension do
end

@impl GenServer
def handle_info({:runtime_ready, _path, _pid}, state) do
{:noreply, state}
end

def handle_info({:compiler, diagnostics}, state) when is_list(diagnostics) do
DiagnosticCache.clear(state.cache, :elixir)

Expand Down
4 changes: 3 additions & 1 deletion lib/next_ls/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule NextLS.Runtime do
on_initialized = Keyword.fetch!(opts, :on_initialized)
db = Keyword.fetch!(opts, :db)

Registry.register(registry, :runtimes, %{name: name, uri: uri, db: db})
Registry.register(registry, :runtimes, %{name: name, uri: uri, path: working_dir, db: db})

pid =
cond do
Expand Down Expand Up @@ -214,6 +214,8 @@ defmodule NextLS.Runtime do
for {pid, _} <- entries, do: send(pid, {:compiler, diagnostics})
end)

NextLS.Logger.log(state.logger, "Compiled #{state.name}!")

diagnostics

unknown ->
Expand Down
9 changes: 9 additions & 0 deletions priv/monkey/_next_ls_private_credo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule :_next_ls_private_credo do
@moduledoc false

def issues(dir) do
["--strict", "--all", "--working-dir", dir]
|> Credo.run()
|> Credo.Execution.get_issues()
end
end
5 changes: 0 additions & 5 deletions test/next_ls/dependency_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ defmodule NextLS.DependencyTest do
def bar() do
42
end

def call_baz() do
Baz.baz()
end
end
""")

Expand Down Expand Up @@ -264,7 +260,6 @@ defmodule NextLS.DependencyTest do
elixir: "~> 1.10",
deps: [
{:bar, path: "../bar"},
{:baz, path: "../baz"}
]
]
end
Expand Down
Loading