Skip to content

Commit cc190ea

Browse files
committed
feat(extension): credo
1 parent d53d8e4 commit cc190ea

File tree

5 files changed

+187
-5
lines changed

5 files changed

+187
-5
lines changed

lib/next_ls.ex

Lines changed: 22 additions & 4 deletions
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

@@ -339,7 +340,11 @@ defmodule NextLS do
339340
{:ok, _} =
340341
DynamicSupervisor.start_child(
341342
lsp.assigns.dynamic_supervisor,
342-
{extension, cache: lsp.assigns.cache, registry: lsp.assigns.registry, publisher: self()}
343+
{extension,
344+
cache: lsp.assigns.cache,
345+
registry: lsp.assigns.registry,
346+
publisher: self(),
347+
task_supervisor: lsp.assigns.runtime_task_supervisor}
343348
)
344349
end
345350

@@ -387,7 +392,14 @@ defmodule NextLS do
387392
if status == :ready do
388393
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
389394
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
390-
send(parent, {:runtime_ready, name, self()})
395+
396+
msg = {:runtime_ready, name, self()}
397+
398+
dispatch(lsp.assigns.registry, :extensions, fn entries ->
399+
for {pid, _} <- entries, do: send(pid, msg)
400+
end)
401+
402+
send(parent, msg)
391403
else
392404
Progress.stop(lsp, token)
393405
GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize")
@@ -502,7 +514,13 @@ defmodule NextLS do
502514
if status == :ready do
503515
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
504516
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
505-
send(parent, {:runtime_ready, name, self()})
517+
msg = {:runtime_ready, name, self()}
518+
519+
dispatch(lsp.assigns.registry, :extensions, fn entries ->
520+
for {pid, _} <- entries, do: send(pid, msg)
521+
end)
522+
523+
send(parent, msg)
506524
else
507525
Progress.stop(lsp, token)
508526
GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize")
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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: [],
33+
cache: cache,
34+
registry: registry,
35+
task_supervisor: task_supervisor,
36+
publisher: publisher,
37+
refresh_refs: Map.new()
38+
}}
39+
end
40+
41+
def handle_info({:runtime_ready, _name, runtime_pid}, state) do
42+
state =
43+
case Runtime.call(runtime_pid, {Code, :ensure_loaded?, [Credo]}) do
44+
{:ok, true} ->
45+
:next_ls
46+
|> :code.priv_dir()
47+
|> Path.join("monkey/_next_ls_private_credo.ex")
48+
|> then(&Runtime.call(runtime_pid, {Code, :compile_file, [&1]}))
49+
50+
Runtime.call(runtime_pid, {Application, :ensure_all_started, [:credo]})
51+
52+
Runtime.call(runtime_pid, {GenServer, :call, [Credo.CLI.Output.Shell, {:suppress_output, true}]})
53+
54+
update_in(state.runtimes, &[runtime_pid | &1])
55+
56+
_ ->
57+
state
58+
end
59+
60+
{:noreply, state}
61+
end
62+
63+
@impl GenServer
64+
def handle_info({:compiler, _diagnostics}, state) do
65+
refresh_refs =
66+
dispatch(state.registry, :runtimes, fn entries ->
67+
for {runtime, %{path: path}} <- entries, runtime in state.runtimes, into: %{} do
68+
namespace = {:credo, path}
69+
DiagnosticCache.clear(state.cache, namespace)
70+
71+
task =
72+
Task.Supervisor.async_nolink(state.task_supervisor, fn ->
73+
case Runtime.call(runtime, {:_next_ls_private_credo, :issues, [path]}) do
74+
{:ok, issues} -> issues
75+
_error -> []
76+
end
77+
end)
78+
79+
{task.ref, namespace}
80+
end
81+
end)
82+
83+
send(state.publisher, :publish)
84+
85+
{:noreply, put_in(state.refresh_refs, refresh_refs)}
86+
end
87+
88+
def handle_info({ref, issues}, %{refresh_refs: refs} = state) when is_map_key(refs, ref) do
89+
Process.demonitor(ref, [:flush])
90+
{{:credo, path} = namespace, refs} = Map.pop(refs, ref)
91+
92+
for issue <- issues do
93+
diagnostic = %Diagnostic{
94+
range: %Range{
95+
start: %Position{
96+
line: issue.line_no - 1,
97+
character: (issue.column || 1) - 1
98+
},
99+
end: %Position{
100+
line: issue.line_no - 1,
101+
character: issue.column || 1
102+
}
103+
},
104+
severity: category_to_severity(issue.category),
105+
data: %{check: issue.check, file: issue.filename},
106+
source: "credo",
107+
code: Macro.to_string(issue.check),
108+
code_description: %CodeDescription{
109+
href: "https://hexdocs.pm/credo/#{Macro.to_string(issue.check)}.html"
110+
},
111+
message: issue.message
112+
}
113+
114+
DiagnosticCache.put(state.cache, namespace, Path.join(path, issue.filename), diagnostic)
115+
end
116+
117+
send(state.publisher, :publish)
118+
119+
{:noreply, put_in(state.refresh_refs, refs)}
120+
end
121+
122+
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{refresh_refs: refs} = state) when is_map_key(refs, ref) do
123+
{_, refs} = Map.pop(refs, ref)
124+
125+
{:noreply, put_in(state.refresh_refs, refs)}
126+
end
127+
128+
defp dispatch(registry, key, callback) do
129+
ref = make_ref()
130+
me = self()
131+
132+
Registry.dispatch(registry, key, fn entries ->
133+
result = callback.(entries)
134+
135+
send(me, {ref, result})
136+
end)
137+
138+
receive do
139+
{^ref, result} -> result
140+
end
141+
end
142+
143+
defp category_to_severity(:refactor), do: DiagnosticSeverity.error()
144+
defp category_to_severity(:warning), do: DiagnosticSeverity.warning()
145+
defp category_to_severity(:design), do: DiagnosticSeverity.information()
146+
147+
defp category_to_severity(:consistency), do: DiagnosticSeverity.information()
148+
149+
defp category_to_severity(:readability), do: DiagnosticSeverity.information()
150+
end

lib/next_ls/extensions/elixir_extension.ex

Lines changed: 4 additions & 0 deletions
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

lib/next_ls/runtime.ex

Lines changed: 1 addition & 1 deletion
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

priv/monkey/_next_ls_private_credo.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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
10+

0 commit comments

Comments
 (0)