elixir-ls
elixir-ls copied to clipboard
Mix.Project.config/0 sometimes doesn't return the project configuration
Environment
- Elixir & Erlang versions (elixir --version): 1.13, OTP 24
- Elixir Language Server version: 0.10.0
- Operating system: Ubuntu
- Editor or IDE name (e.g. Emacs/VSCode): VSCode
- Editor Plugin/LSP Client name and version: ElixirLS
Current behavior
I'm investigating crashes in the boundary library. These crashes take place only when the project is built inside ElixirLS.
For the purpose of this report, it's enough to know that boundary is a custom compiler which invokes Mix.Project.config/0 to get the project configuration.
It appears that in some situations, Mix.Project.config/0 isn't returning the project config, but rather some generic default template. As the docs state: If there is no project defined, it still returns a keyword list with default values. This allows many Mix tasks to work without the need for an underlying project.. This is only happening in ElixirLS, and only after the previous compilation failed due to an error.
Here are steps to reproduce:
-
mix new my_app -
In mix.exs add
{:boundary, "~> 0.9"}as a dep, and fetch deps. -
In my_app.ex add
use Boundaryto theMyAppmodule. -
In mix.exs add
compilers: [:boundary | Mix.compilers()]inproject/0. Only do this after step 3, or otherwise ElixirLS will crash due to another bug (which I need to fix in the boundary lib).Make sure that the project is compiling from ElixirLS (I used vscode for testing, and observed the output view).
-
Introduce a compilation error in my_app.ex, e.g.:
def hello do foobar() :world1 end -
Wait until the compilation fails, then fix the error. At this point, the boundary compiler will crash with:
** (KeyError) key :app not found in: [aliases: [], build_embedded: false, build_per_environment: true, build_scm: Mix.SCM.Path, config_path: "config/config.exs", consolidate_protocols: true, default_task: "run", deps: [], deps_path: "deps", elixirc_paths: ["lib"], erlc_paths: ["src"], erlc_include_path: "include", erlc_options: [], lockfile: "mix.lock", preferred_cli_env: [], start_permanent: false] (elixir 1.13.1) lib/keyword.ex:559: Keyword.fetch!/2 (boundary 0.9.3) lib/boundary/mix/xref.ex:123: Boundary.Mix.Xref.seen_table/0 (boundary 0.9.3) lib/boundary/mix/xref.ex:34: Boundary.Mix.Xref.initialize_module/1 (boundary 0.9.3) lib/boundary/mix/tasks/compile/boundary.ex:128: Mix.Tasks.Compile.Boundary.record/5
Notice the keyword list which is inspected in this output. This list is obtained via Mix.Project.config(), but it doesn't correspond to the actual project config. For example the :app key is missing, as well as the :compilers key.
Restarting ElixirLS will fix the issue. I wasn't able to reproduce the problem outside of ElixirLS. Due to this, it seems that the problem is somehow related to the way ElixirLS builds the project after a failed compilation.
Expected behavior
Mix.Project.config/0 should always return the correct project configuration.
@sasa1977 do you have this error in a phoenix project with reloader? It uses a similar approach to recompilation.
@sasa1977 do you have this error in a phoenix project with reloader? It uses a similar approach to recompilation.
Just tried the following:
- mix phx.new (latest archive)
- Add boundary, setup web and main boundaries (both with
check: [in: false, out: false]options, to avoid boundary errors) - Start
iex -S mix phx.server, visit localhost:4000 - in
PageControllerintroduce a compilation error, refresh the browser page - Fix the compilation error, refresh the page again
Result:
- Phoenix recompiles the single file successfully
- ElixirLS recompilation crashes, as describe above
Adding a data point - I have this issue as well
elixir 1.12.3-otp-24 erlang 24.1 on Mac Pro M1 VSCode, ElxiirLS v0.11.0
While we wait for a fix, here is a workaround (no sure if this is the best env var to check for, but it seems to work)
# Workaround to avoid Boundary breaking ElixirLS:
# ElixirLS runs with the ELS_MODE env var set, so we disable the
# Boundary compiler in that case.
# See https://github.com/elixir-lsp/elixir-ls/issues/717
@use_boundary (if System.get_env("ELS_MODE") do
[]
else
[:boundary]
end)
def project do
[
# ...
compilers: @use_boundary ++ [:other :compilers] ++ Mix.compilers(),
# ...
]
end
@sasa1977 after some debugging this looks like a problem in boundary exposed by elixir-ls. We reload and recompile MixProject on every build to pick up changes in mix config. During normal successful compilation everything works as expected. See
Mix.ProjectStack.peek() on MixProject reload: %{
config: [
app: :boundary_crash,
],
}
Mix.ProjectStack.peek() after pop: nil
Mix.ProjectStack.peek() after post config: nil
Code.get_compiler_options: %{
tracers: [ElixirLS.LanguageServer.Tracer],
}
Mix.ProjectStack.peek() after load config: %{
config: [
app: :boundary_crash,
],
}
------boundary compiler executes
Compiling 1 file (.ex)
Here's what happens when the build fails
Code.get_compiler_options: %{
tracers: [ElixirLS.LanguageServer.Tracer],
}
Mix.ProjectStack.peek() after load config: %{
config: [
app: :boundary_crash,
],
}
------boundary compiler executes
Compiling 1 file (.ex)
== Compilation error in file lib/boundary_crash.ex ==
** (CompileError) lib/boundary_crash.ex:17: undefined function foobar/0 (expected BoundaryCrash to define such a function or for it to be imported, but none are available)
And then after fixing build
Mix.ProjectStack.peek() after post config: nil
Code.get_compiler_options: %{
tracers: [Mix.Tasks.Compile.Boundary, ElixirLS.LanguageServer.Tracer],
}
== Compilation error in file mix.exs ==
** (KeyError) key :app not found in: [aliases: [], build_embedded: false, build_per_environment: true, build_scm: Mix.SCM.Path, config_path: "config/config.exs", consolidate_protocols: true, default_task: "run", deps: [], deps_path: "deps", elixirc_paths: ["lib"], erlc_paths: ["src"], erlc_include_path: "include", erlc_options: [], lockfile: "mix.lock", preferred_cli_env: [], start_permanent: false]
boundary is not properly clearing its tracer from compiler_options on failed builds and when elixir-ls tries to reload and recompile MixProject this tracer crashes. While elixir-ls could clean up tracers and reset it to known state I think this responsibility should be on boundary.
Hmm it may be difficult to properly implement cleanup as it seems that when build fails Mix.Task.Compiler.after_compiler callback is not invoked (tested on a simple vanilla mix app and a simple noop compiler task).
Edit: reported upstream https://github.com/elixir-lang/elixir/issues/12159
Thanks for debugging! Looking at the boundary code, it doesn't seem correct. The tracer is cleared on success, but not on error.
I'll try the fix tomorrow and report back, although your follow-up report suggests that an Elixir bug also has to be fixed.
Either way, I'm a bit confused, because this used to work just fine up until recently. I suppose something has changed in the way ElixirLS recompiles the project, right?
Yeah, I've fixed the behaviour in boundary, but the error still appears, because the callback is not invoked, so I guess we'll have to wait for the Elixir fix.
I'll try the workaround with clearing tracers
FYI, I took the approach advised in https://github.com/elixir-lang/elixir/issues/12159#issuecomment-1273584316 (cleaning up the tracers after the Elixir compiler) and this resolved the issue. The fix is published on hex in boundary 0.9.4. Thanks for helping troubleshoot this.