Skip to content

Commit 3551de8

Browse files
committed
cache Mix.Project data and make all accesses to it safe from Mix.State push/pop during compile
1 parent 1a3ae77 commit 3551de8

File tree

6 files changed

+212
-40
lines changed

6 files changed

+212
-40
lines changed

apps/language_server/lib/language_server.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ defmodule ElixirLS.LanguageServer do
1414
{ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc},
1515
{ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []},
1616
{ElixirLS.LanguageServer.Tracer, []},
17+
{ElixirLS.LanguageServer.MixProject, []},
1718
{ElixirLS.LanguageServer.ExUnitTestTracer, []}
1819
]
1920
|> Enum.reject(&is_nil/1)

apps/language_server/lib/language_server/build.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule ElixirLS.LanguageServer.Build do
1515

1616
case reload_project() do
1717
{:ok, mixfile_diagnostics} ->
18+
ElixirLS.LanguageServer.MixProject.store()
1819
# FIXME: Private API
1920

2021
try do
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
defmodule ElixirLS.LanguageServer.MixProject do
2+
@moduledoc """
3+
This module serves as a caching layer guarantying a safe access to Mix.Project functions. Note that
4+
Mix.Project functions cannot be safely called during a build as dep mix projects are being pushed and
5+
popped
6+
"""
7+
use GenServer
8+
9+
def start_link(args) do
10+
GenServer.start_link(__MODULE__, args, name: __MODULE__)
11+
end
12+
13+
def store do
14+
GenServer.call(__MODULE__, :store)
15+
end
16+
17+
@spec get() :: module | nil
18+
def get do
19+
GenServer.call(__MODULE__, {:get, :get})
20+
end
21+
22+
@spec get!() :: module
23+
def get! do
24+
get() || raise Mix.NoProjectError, []
25+
end
26+
27+
@spec project_file() :: binary | nil
28+
def project_file() do
29+
GenServer.call(__MODULE__, {:get, :project_file})
30+
end
31+
32+
# @doc since: "1.15.0"
33+
# @spec parent_umbrella_project_file() :: binary | nil
34+
# defdelegate parent_umbrella_project_file(), to: Mix.ProjectStack
35+
36+
@spec config() :: keyword
37+
def config do
38+
GenServer.call(__MODULE__, {:get, :config})
39+
end
40+
41+
@spec config_files() :: [Path.t()]
42+
def config_files do
43+
GenServer.call(__MODULE__, {:get, :config_files})
44+
end
45+
46+
@spec config_mtime() :: posix_mtime when posix_mtime: integer()
47+
def config_mtime do
48+
GenServer.call(__MODULE__, {:get, :config_mtime})
49+
end
50+
51+
@spec umbrella?() :: boolean
52+
def umbrella?() do
53+
GenServer.call(__MODULE__, {:get, :umbrella?})
54+
end
55+
56+
@spec apps_paths() :: %{optional(atom) => Path.t()} | nil
57+
def apps_paths() do
58+
GenServer.call(__MODULE__, {:get, :apps_paths})
59+
end
60+
61+
@spec deps_path() :: Path.t()
62+
def deps_path() do
63+
GenServer.call(__MODULE__, {:get, :deps_path})
64+
end
65+
66+
@spec deps_apps() :: [atom()]
67+
def deps_apps() do
68+
GenServer.call(__MODULE__, {:get, :deps_apps})
69+
end
70+
71+
@spec deps_scms() :: %{optional(atom) => Mix.SCM.t()}
72+
def deps_scms() do
73+
GenServer.call(__MODULE__, {:get, :deps_scms})
74+
end
75+
76+
@spec deps_paths() :: %{optional(atom) => Path.t()}
77+
def deps_paths() do
78+
GenServer.call(__MODULE__, {:get, :deps_paths})
79+
end
80+
81+
# @doc since: "1.15.0"
82+
# @spec deps_tree(keyword) :: %{optional(atom) => [atom]}
83+
# def deps_tree(opts \\ []) when is_list(opts) do
84+
# traverse_deps(opts, fn %{deps: deps} -> Enum.map(deps, & &1.app) end)
85+
# end
86+
87+
@spec build_path() :: Path.t()
88+
def build_path() do
89+
GenServer.call(__MODULE__, {:get, :build_path})
90+
end
91+
92+
@spec manifest_path() :: Path.t()
93+
def manifest_path() do
94+
GenServer.call(__MODULE__, {:get, :manifest_path})
95+
end
96+
97+
@spec app_path() :: Path.t()
98+
def app_path() do
99+
config = config()
100+
config[:deps_app_path] ||
101+
cond do
102+
app = config[:app] ->
103+
Path.join([build_path(), "lib", Atom.to_string(app)])
104+
105+
config[:apps_path] ->
106+
raise "trying to access Mix.Project.app_path/1 for an umbrella project but umbrellas have no app"
107+
108+
true ->
109+
Mix.raise(
110+
"Cannot access build without an application name, " <>
111+
"please ensure you are in a directory with a mix.exs file and it defines " <>
112+
"an :app name under the project configuration"
113+
)
114+
end
115+
end
116+
117+
@spec compile_path() :: Path.t()
118+
def compile_path() do
119+
Path.join(app_path(), "ebin")
120+
end
121+
122+
@spec consolidation_path() :: Path.t()
123+
def consolidation_path() do
124+
GenServer.call(__MODULE__, {:get, :consolidation_path})
125+
end
126+
127+
@impl GenServer
128+
def init(_) do
129+
{:ok, nil}
130+
end
131+
132+
@impl GenServer
133+
def handle_call({:get, key}, _from, state) do
134+
{:reply, Map.fetch!(state, key), state}
135+
end
136+
137+
def handle_call(:store, _from, _state) do
138+
state = %{
139+
get: Mix.Project.get(),
140+
project_file: Mix.Project.project_file(),
141+
config: Mix.Project.config(),
142+
config_files: Mix.Project.config_files(),
143+
config_mtime: Mix.Project.config_mtime(),
144+
umbrella?: Mix.Project.umbrella?(),
145+
apps_paths: Mix.Project.apps_paths(),
146+
deps_path: Mix.Project.deps_path(),
147+
deps_apps: Mix.Project.deps_apps(),
148+
deps_scms: Mix.Project.deps_scms(),
149+
deps_paths: Mix.Project.deps_paths(),
150+
build_path: Mix.Project.build_path(),
151+
manifest_path: Mix.Project.manifest_path(),
152+
consolidation_path: Mix.Project.consolidation_path()
153+
}
154+
155+
{:reply, :ok, state}
156+
end
157+
end

apps/language_server/lib/language_server/mix_tasks/format.ex

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,13 @@ defmodule Mix.Tasks.ElixirLSFormat do
253253
Mix.raise("--no-exit can only be used together with --check-formatted")
254254
end
255255

256+
deps_paths = Mix.Project.deps_paths()
257+
manifest_path = Mix.Project.manifest_path()
258+
config_mtime = Mix.Project.config_mtime()
259+
mix_project = Mix.Project.get()
260+
256261
{formatter_opts_and_subs, _sources} =
257-
eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter])
262+
eval_deps_and_subdirectories(cwd, mix_project, deps_paths, manifest_path, config_mtime, dot_formatter, formatter_opts, [dot_formatter])
258263

259264
formatter_opts_and_subs = load_plugins(formatter_opts_and_subs)
260265

@@ -326,10 +331,14 @@ defmodule Mix.Tasks.ElixirLSFormat do
326331
@doc since: "1.13.0"
327332
def formatter_for_file(file, opts \\ []) do
328333
cwd = Keyword.get_lazy(opts, :root, &File.cwd!/0)
334+
deps_paths = Keyword.get_lazy(opts, :deps_paths, &Mix.Project.deps_paths/0)
335+
manifest_path = Keyword.get_lazy(opts, :manifest_path, &Mix.Project.manifest_path/0)
336+
config_mtime = Keyword.get_lazy(opts, :config_mtime, &Mix.Project.config_mtime/0)
337+
mix_project = Keyword.get_lazy(opts, :mix_project, &Mix.Project.get/0)
329338
{dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts)
330339

331340
{formatter_opts_and_subs, _sources} =
332-
eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter])
341+
eval_deps_and_subdirectories(cwd, mix_project, deps_paths, manifest_path, config_mtime, dot_formatter, formatter_opts, [dot_formatter])
333342

334343
formatter_opts_and_subs = load_plugins(formatter_opts_and_subs)
335344

@@ -363,7 +372,7 @@ defmodule Mix.Tasks.ElixirLSFormat do
363372
# This function reads exported configuration from the imported
364373
# dependencies and subdirectories and deals with caching the result
365374
# of reading such configuration in a manifest file.
366-
defp eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, sources) do
375+
defp eval_deps_and_subdirectories(cwd, mix_project, deps_paths, manifest_path, config_mtime, dot_formatter, formatter_opts, sources) do
367376
deps = Keyword.get(formatter_opts, :import_deps, [])
368377
subs = Keyword.get(formatter_opts, :subdirectories, [])
369378

@@ -378,12 +387,12 @@ defmodule Mix.Tasks.ElixirLSFormat do
378387
if deps == [] and subs == [] do
379388
{{formatter_opts, []}, sources}
380389
else
381-
manifest = Path.join(Mix.Project.manifest_path(), @manifest)
390+
manifest = Path.join(manifest_path, @manifest)
382391

383392
{{locals_without_parens, subdirectories}, sources} =
384-
maybe_cache_in_manifest(dot_formatter, manifest, fn ->
385-
{subdirectories, sources} = eval_subs_opts(subs, cwd, sources)
386-
{{eval_deps_opts(deps), subdirectories}, sources}
393+
maybe_cache_in_manifest(dot_formatter, mix_project, manifest, config_mtime, fn ->
394+
{subdirectories, sources} = eval_subs_opts(subs, cwd, mix_project, deps_paths, manifest_path, config_mtime, sources)
395+
{{eval_deps_opts(deps, deps_paths), subdirectories}, sources}
387396
end)
388397

389398
formatter_opts =
@@ -398,19 +407,19 @@ defmodule Mix.Tasks.ElixirLSFormat do
398407
end
399408
end
400409

401-
defp maybe_cache_in_manifest(dot_formatter, manifest, fun) do
410+
defp maybe_cache_in_manifest(dot_formatter, mix_project, manifest, config_mtime, fun) do
402411
cond do
403-
is_nil(Mix.Project.get()) or dot_formatter != ".formatter.exs" -> fun.()
404-
entry = read_manifest(manifest) -> entry
412+
is_nil(mix_project) or dot_formatter != ".formatter.exs" -> fun.()
413+
entry = read_manifest(manifest, config_mtime) -> entry
405414
true -> write_manifest!(manifest, fun.())
406415
end
407416
end
408417

409-
defp read_manifest(manifest) do
418+
defp read_manifest(manifest, config_mtime) do
410419
with {:ok, binary} <- File.read(manifest),
411420
{:ok, {@manifest_vsn, entry, sources}} <- safe_binary_to_term(binary),
412421
expanded_sources = Enum.flat_map(sources, &Path.wildcard(&1, match_dot: true)),
413-
false <- Mix.Utils.stale?([Mix.Project.config_mtime() | expanded_sources], [manifest]) do
422+
false <- Mix.Utils.stale?([config_mtime | expanded_sources], [manifest]) do
414423
{entry, sources}
415424
else
416425
_ -> nil
@@ -429,13 +438,11 @@ defmodule Mix.Tasks.ElixirLSFormat do
429438
{entry, sources}
430439
end
431440

432-
defp eval_deps_opts([]) do
441+
defp eval_deps_opts([], _deps_paths) do
433442
[]
434443
end
435444

436-
defp eval_deps_opts(deps) do
437-
deps_paths = Mix.Project.deps_paths()
438-
445+
defp eval_deps_opts(deps, deps_paths) do
439446
for dep <- deps,
440447
dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths),
441448
dep_dot_formatter = Path.join(dep_path, ".formatter.exs"),
@@ -446,7 +453,7 @@ defmodule Mix.Tasks.ElixirLSFormat do
446453
do: parenless_call
447454
end
448455

449-
defp eval_subs_opts(subs, cwd, sources) do
456+
defp eval_subs_opts(subs, cwd, mix_project, deps_paths, manifest_path, config_mtime, sources) do
450457
{subs, sources} =
451458
Enum.flat_map_reduce(subs, sources, fn sub, sources ->
452459
cwd = Path.expand(sub, cwd)
@@ -460,7 +467,7 @@ defmodule Mix.Tasks.ElixirLSFormat do
460467
formatter_opts = eval_file_with_keyword_list(sub_formatter)
461468

462469
{formatter_opts_and_subs, sources} =
463-
eval_deps_and_subdirectories(sub, :in_memory, formatter_opts, sources)
470+
eval_deps_and_subdirectories(sub, mix_project, deps_paths, manifest_path, config_mtime, :in_memory, formatter_opts, sources)
464471

465472
{[{sub, formatter_opts_and_subs}], sources}
466473
else

apps/language_server/lib/language_server/server.ex

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,34 +1127,38 @@ defmodule ElixirLS.LanguageServer.Server do
11271127
end
11281128

11291129
defp get_spec_code_lenses(state = %__MODULE__{}, uri, source_file) do
1130-
if dialyzer_enabled?(state) and !!state.settings["suggestSpecs"] do
1130+
if is_binary(state.project_dir) and dialyzer_enabled?(state) and !!state.settings["suggestSpecs"] do
11311131
CodeLens.spec_code_lens(state.server_instance_id, uri, source_file.text)
11321132
else
11331133
{:ok, []}
11341134
end
11351135
end
11361136

11371137
defp get_test_code_lenses(state = %__MODULE__{}, uri, source_file) do
1138-
get_test_code_lenses(
1139-
state,
1140-
uri,
1141-
source_file,
1142-
state.settings["enableTestLenses"] || false,
1143-
Mix.Project.umbrella?()
1144-
)
1138+
enabled = state.settings["enableTestLenses"] || false
1139+
1140+
if is_binary(state.project_dir) and enabled do
1141+
get_test_code_lenses(
1142+
state,
1143+
uri,
1144+
source_file,
1145+
ElixirLS.LanguageServer.MixProject.umbrella?()
1146+
)
1147+
else
1148+
{:ok, []}
1149+
end
11451150
end
11461151

11471152
defp get_test_code_lenses(
11481153
state = %__MODULE__{project_dir: project_dir},
11491154
"file:" <> _ = uri,
11501155
source_file,
1151-
true = _enabled,
11521156
true = _umbrella
11531157
)
11541158
when is_binary(project_dir) do
11551159
file_path = SourceFile.Path.from_uri(uri)
11561160

1157-
Mix.Project.apps_paths()
1161+
ElixirLS.LanguageServer.MixProject.apps_paths()
11581162
|> Enum.find(fn {_app, app_path} -> under_app?(file_path, project_dir, app_path) end)
11591163
|> case do
11601164
nil ->
@@ -1173,7 +1177,6 @@ defmodule ElixirLS.LanguageServer.Server do
11731177
%__MODULE__{project_dir: project_dir},
11741178
"file:" <> _ = uri,
11751179
source_file,
1176-
true = _enabled,
11771180
false = _umbrella
11781181
)
11791182
when is_binary(project_dir) do
@@ -1190,7 +1193,7 @@ defmodule ElixirLS.LanguageServer.Server do
11901193
end
11911194
end
11921195

1193-
defp get_test_code_lenses(%__MODULE__{}, _uri, _source_file, _, _), do: {:ok, []}
1196+
defp get_test_code_lenses(%__MODULE__{}, _uri, _source_file, _), do: {:ok, []}
11941197

11951198
defp is_test_file?(file_path, state = %__MODULE__{project_dir: project_dir}, app, app_path)
11961199
when is_binary(project_dir) do
@@ -1209,8 +1212,9 @@ defmodule ElixirLS.LanguageServer.Server do
12091212
end
12101213

12111214
defp is_test_file?(file_path) do
1212-
test_paths = Mix.Project.config()[:test_paths] || ["test"]
1213-
test_pattern = Mix.Project.config()[:test_pattern] || "*_test.exs"
1215+
config = ElixirLS.LanguageServer.MixProject.config()
1216+
test_paths = config[:test_paths] || ["test"]
1217+
test_pattern = config[:test_pattern] || "*_test.exs"
12141218
file_path = Path.expand(file_path)
12151219

12161220
Mix.Utils.extract_files(test_paths, test_pattern)

apps/language_server/lib/language_server/source_file.ex

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,19 @@ defmodule ElixirLS.LanguageServer.SourceFile do
230230
end
231231

232232
@spec formatter_for(String.t(), String.t() | nil) :: {:ok, {function | nil, keyword(), String.t}} | :error
233-
def formatter_for(uri = "file:" <> _, project_dir) do
233+
def formatter_for(uri = "file:" <> _, project_dir) when is_binary(project_dir) do
234234
path = __MODULE__.Path.from_uri(uri)
235235

236236
try do
237-
true = Code.ensure_loaded?(Mix.Tasks.ElixirLSFormat)
238-
239-
if project_dir do
240-
{:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path, root: project_dir)}
241-
else
242-
{:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path)}
243-
end
237+
alias ElixirLS.LanguageServer.MixProject
238+
opts = [
239+
deps_paths: MixProject.deps_paths(),
240+
manifest_path: MixProject.manifest_path(),
241+
config_mtime: MixProject.config_mtime(),
242+
mix_project: MixProject.get(),
243+
root: project_dir
244+
]
245+
{:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path, opts)}
244246
catch
245247
kind, payload ->
246248
{payload, stacktrace} = Exception.blame(kind, payload, __STACKTRACE__)

0 commit comments

Comments
 (0)