Skip to content

Commit 70d52dc

Browse files
authored
feat(extension): credo (#163)
Proves Credo diagnostics for projects that include Credo. Closes #16 TODO: Code actions
1 parent 9b8106b commit 70d52dc

File tree

11 files changed

+318
-17
lines changed

11 files changed

+318
-17
lines changed

Diff for: lib/next_ls.ex

+22-6
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ defmodule NextLS do
6363
dynamic_supervisor = Keyword.fetch!(args, :dynamic_supervisor)
6464

6565
registry = Keyword.fetch!(args, :registry)
66-
extensions = Keyword.get(args, :extensions, [NextLS.ElixirExtension])
66+
67+
extensions = Keyword.get(args, :extensions, [NextLS.ElixirExtension, NextLS.CredoExtension])
6768
cache = Keyword.fetch!(args, :cache)
6869
{:ok, logger} = DynamicSupervisor.start_child(dynamic_supervisor, {NextLS.Logger, lsp: lsp})
6970

@@ -363,7 +364,11 @@ defmodule NextLS do
363364
{:ok, _} =
364365
DynamicSupervisor.start_child(
365366
lsp.assigns.dynamic_supervisor,
366-
{extension, cache: lsp.assigns.cache, registry: lsp.assigns.registry, publisher: self()}
367+
{extension,
368+
cache: lsp.assigns.cache,
369+
registry: lsp.assigns.registry,
370+
publisher: self(),
371+
task_supervisor: lsp.assigns.runtime_task_supervisor}
367372
)
368373
end
369374

@@ -411,7 +416,14 @@ defmodule NextLS do
411416
if status == :ready do
412417
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
413418
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
414-
send(parent, {:runtime_ready, name, self()})
419+
420+
msg = {:runtime_ready, name, self()}
421+
422+
dispatch(lsp.assigns.registry, :extensions, fn entries ->
423+
for {pid, _} <- entries, do: send(pid, msg)
424+
end)
425+
426+
send(parent, msg)
415427
else
416428
Progress.stop(lsp, token)
417429
GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize")
@@ -526,7 +538,13 @@ defmodule NextLS do
526538
if status == :ready do
527539
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
528540
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
529-
send(parent, {:runtime_ready, name, self()})
541+
msg = {:runtime_ready, name, self()}
542+
543+
dispatch(lsp.assigns.registry, :extensions, fn entries ->
544+
for {pid, _} <- entries, do: send(pid, msg)
545+
end)
546+
547+
send(parent, msg)
530548
else
531549
Progress.stop(lsp, token)
532550
GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize")
@@ -596,8 +614,6 @@ defmodule NextLS do
596614
end
597615

598616
def handle_info(:publish, lsp) do
599-
GenLSP.log(lsp, "[NextLS] Compiled!")
600-
601617
all =
602618
for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), {file, diagnostics} <- cache, reduce: %{} do
603619
d -> Map.update(d, file, diagnostics, fn value -> value ++ diagnostics end)

Diff for: lib/next_ls/db.ex

+6
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ defmodule NextLS.DB do
162162
is_atom(arg) and String.starts_with?(to_string(arg), "Elixir.") ->
163163
Macro.to_string(arg)
164164

165+
arg in [nil, :undefined] ->
166+
arg
167+
168+
is_atom(arg) ->
169+
to_string(arg)
170+
165171
true ->
166172
arg
167173
end

Diff for: lib/next_ls/extensions/credo_extension.ex

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
defmodule NextLS.CredoExtension do
2+
@moduledoc false
3+
use GenServer
4+
5+
alias GenLSP.Enumerations.DiagnosticSeverity
6+
alias GenLSP.Structures.CodeDescription
7+
alias GenLSP.Structures.Diagnostic
8+
alias GenLSP.Structures.Position
9+
alias GenLSP.Structures.Range
10+
alias NextLS.DiagnosticCache
11+
alias NextLS.Runtime
12+
13+
def start_link(args) do
14+
GenServer.start_link(
15+
__MODULE__,
16+
Keyword.take(args, [:cache, :registry, :publisher, :task_supervisor]),
17+
Keyword.take(args, [:name])
18+
)
19+
end
20+
21+
@impl GenServer
22+
def init(args) do
23+
cache = Keyword.fetch!(args, :cache)
24+
registry = Keyword.fetch!(args, :registry)
25+
publisher = Keyword.fetch!(args, :publisher)
26+
task_supervisor = Keyword.fetch!(args, :task_supervisor)
27+
28+
Registry.register(registry, :extensions, :credo)
29+
30+
{:ok,
31+
%{
32+
runtimes: Map.new(),
33+
cache: cache,
34+
registry: registry,
35+
task_supervisor: task_supervisor,
36+
publisher: publisher,
37+
refresh_refs: Map.new()
38+
}}
39+
end
40+
41+
@impl GenServer
42+
43+
def handle_info({:runtime_ready, _, _}, state), do: {:noreply, state}
44+
45+
def handle_info({:compiler, _diagnostics}, state) do
46+
{state, refresh_refs} =
47+
dispatch(state.registry, :runtimes, fn entries ->
48+
# loop over runtimes
49+
for {runtime, %{path: path}} <- entries, reduce: {state, %{}} do
50+
{state, refs} ->
51+
# determine the existence of Credo and memoize the result
52+
state =
53+
if not Map.has_key?(state.runtimes, runtime) do
54+
case Runtime.call(runtime, {Code, :ensure_loaded?, [Credo]}) do
55+
{:ok, true} ->
56+
:next_ls
57+
|> :code.priv_dir()
58+
|> Path.join("monkey/_next_ls_private_credo.ex")
59+
|> then(&Runtime.call(runtime, {Code, :compile_file, [&1]}))
60+
61+
Runtime.call(runtime, {Application, :ensure_all_started, [:credo]})
62+
Runtime.call(runtime, {GenServer, :call, [Credo.CLI.Output.Shell, {:suppress_output, true}]})
63+
64+
put_in(state.runtimes[runtime], true)
65+
66+
_ ->
67+
state
68+
end
69+
else
70+
state
71+
end
72+
73+
# if runtime has Credo
74+
if state.runtimes[runtime] do
75+
namespace = {:credo, path}
76+
DiagnosticCache.clear(state.cache, namespace)
77+
78+
task =
79+
Task.Supervisor.async_nolink(state.task_supervisor, fn ->
80+
case Runtime.call(runtime, {:_next_ls_private_credo, :issues, [path]}) do
81+
{:ok, issues} -> issues
82+
_error -> []
83+
end
84+
end)
85+
86+
{state, Map.put(refs, task.ref, namespace)}
87+
else
88+
{state, refs}
89+
end
90+
end
91+
end)
92+
93+
send(state.publisher, :publish)
94+
95+
{:noreply, put_in(state.refresh_refs, refresh_refs)}
96+
end
97+
98+
def handle_info({ref, issues}, %{refresh_refs: refs} = state) when is_map_key(refs, ref) do
99+
Process.demonitor(ref, [:flush])
100+
{{:credo, path} = namespace, refs} = Map.pop(refs, ref)
101+
102+
for issue <- issues do
103+
diagnostic = %Diagnostic{
104+
range: %Range{
105+
start: %Position{
106+
line: issue.line_no - 1,
107+
character: (issue.column || 1) - 1
108+
},
109+
end: %Position{
110+
line: issue.line_no - 1,
111+
character: 999
112+
}
113+
},
114+
severity: category_to_severity(issue.category),
115+
data: %{check: issue.check, file: issue.filename},
116+
source: "credo",
117+
code: Macro.to_string(issue.check),
118+
code_description: %CodeDescription{
119+
href: "https://hexdocs.pm/credo/#{Macro.to_string(issue.check)}.html"
120+
},
121+
message: issue.message
122+
}
123+
124+
DiagnosticCache.put(state.cache, namespace, Path.join(path, issue.filename), diagnostic)
125+
end
126+
127+
send(state.publisher, :publish)
128+
129+
{:noreply, put_in(state.refresh_refs, refs)}
130+
end
131+
132+
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{refresh_refs: refs} = state) when is_map_key(refs, ref) do
133+
{_, refs} = Map.pop(refs, ref)
134+
135+
{:noreply, put_in(state.refresh_refs, refs)}
136+
end
137+
138+
defp dispatch(registry, key, callback) do
139+
ref = make_ref()
140+
me = self()
141+
142+
Registry.dispatch(registry, key, fn entries ->
143+
result = callback.(entries)
144+
145+
send(me, {ref, result})
146+
end)
147+
148+
receive do
149+
{^ref, result} -> result
150+
end
151+
end
152+
153+
defp category_to_severity(:refactor), do: DiagnosticSeverity.error()
154+
defp category_to_severity(:warning), do: DiagnosticSeverity.warning()
155+
defp category_to_severity(:design), do: DiagnosticSeverity.information()
156+
157+
defp category_to_severity(:consistency), do: DiagnosticSeverity.information()
158+
159+
defp category_to_severity(:readability), do: DiagnosticSeverity.information()
160+
end

Diff for: lib/next_ls/extensions/elixir_extension.ex

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ defmodule NextLS.ElixirExtension do
2424
end
2525

2626
@impl GenServer
27+
def handle_info({:runtime_ready, _path, _pid}, state) do
28+
{:noreply, state}
29+
end
30+
2731
def handle_info({:compiler, diagnostics}, state) when is_list(diagnostics) do
2832
DiagnosticCache.clear(state.cache, :elixir)
2933

Diff for: lib/next_ls/runtime.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ defmodule NextLS.Runtime do
5050
on_initialized = Keyword.fetch!(opts, :on_initialized)
5151
db = Keyword.fetch!(opts, :db)
5252

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

5555
pid =
5656
cond do
@@ -214,6 +214,8 @@ defmodule NextLS.Runtime do
214214
for {pid, _} <- entries, do: send(pid, {:compiler, diagnostics})
215215
end)
216216

217+
NextLS.Logger.log(state.logger, "Compiled #{state.name}!")
218+
217219
diagnostics
218220

219221
unknown ->

Diff for: priv/monkey/_next_ls_private_credo.ex

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule :_next_ls_private_credo do
2+
@moduledoc false
3+
4+
def issues(dir) do
5+
["--strict", "--all", "--working-dir", dir]
6+
|> Credo.run()
7+
|> Credo.Execution.get_issues()
8+
end
9+
end

Diff for: test/next_ls/dependency_test.exs

-5
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ defmodule NextLS.DependencyTest do
5959
def bar() do
6060
42
6161
end
62-
63-
def call_baz() do
64-
Baz.baz()
65-
end
6662
end
6763
""")
6864

@@ -264,7 +260,6 @@ defmodule NextLS.DependencyTest do
264260
elixir: "~> 1.10",
265261
deps: [
266262
{:bar, path: "../bar"},
267-
{:baz, path: "../baz"}
268263
]
269264
]
270265
end

0 commit comments

Comments
 (0)