Skip to content

Commit

Permalink
feat(extension): credo (#163)
Browse files Browse the repository at this point in the history
Proves Credo diagnostics for projects that include Credo.

Closes #16 

TODO: Code actions
  • Loading branch information
mhanberg authored Aug 17, 2023
1 parent 9b8106b commit 70d52dc
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 17 deletions.
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

0 comments on commit 70d52dc

Please sign in to comment.