elixir-ls
elixir-ls copied to clipboard
Stale Struct Types Lead to Dialyzer Warnings
Hello 👋🏼 This could be ElixirLS or dialyzer related. Any help is appreciated. 🙂
Environment
- Elixir & Erlang versions (elixir --version):
- Project:
1.12.3(compiled with Erlang/OTP 24) ElixirLS compiled with Elixir 1.12.3 and erlang 24
- Project:
- Elixir Language Server version:
v0.9.0 - Operating system: macOS 12.4
- Editor or IDE name (e.g. Emacs/VSCode): VSCode
- Editor Plugin/LSP Client name and version: ElixirLS for VSCode, v0.9.0 (with custom compiled ElixirLS)
Current behavior
Dialyzer occasionally ignores changes to a struct type %__MODULE__{...} when dialyzing other modules. For example, if I add a new field to struct type %A{}, and then access that field in another module B, dialyzer warns that the new field "cannot exist in a map of type...".
ElixirLS logs when saving the new field to struct %A{}
MIX_ENV: test
MIX_TARGET: host
Compiling 39 files (.ex)
[Info - 10:29:59 AM] Compile took 3196 milliseconds
[Info - 10:29:59 AM] [ElixirLS WorkspaceSymbols] Updating index...
MIX_ENV: test
MIX_TARGET: host
[Info - 10:30:00 AM] Compile took 895 milliseconds
[Info - 10:30:00 AM] [ElixirLS Dialyzer] Checking for stale beam files
[Info - 10:30:00 AM] [ElixirLS WorkspaceSymbols] 5 modules need reindexing
[Info - 10:30:00 AM] [ElixirLS WorkspaceSymbols] 0 callbacks added to index
[Info - 10:30:00 AM] [ElixirLS WorkspaceSymbols] 5 modules added to index
[Info - 10:30:00 AM] [ElixirLS WorkspaceSymbols] 6 types added to index
[Info - 10:30:00 AM] [ElixirLS WorkspaceSymbols] 91 functions added to index
[Info - 10:30:00 AM] [ElixirLS Dialyzer] Found 72 changed files in 248 milliseconds
[Info - 10:30:01 AM] [ElixirLS Dialyzer] Analyzing 21 modules: [...unrelated modules omitted..., B, A]
[Info - 10:30:02 AM] [ElixirLS Dialyzer] Analysis finished in 1439 milliseconds
[Info - 10:30:02 AM] Dialyzer analysis is up to date
[Info - 10:30:03 AM] [ElixirLS Dialyzer] Writing manifest...
[Info - 10:30:04 AM] [ElixirLS Dialyzer] Done writing manifest in 2058 milliseconds.
ElixirLS logs when saving module B, which accesses the new field of type %A{}
MIX_ENV: test
MIX_TARGET: host
Compiling 38 files (.ex)
[Info - 10:30:19 AM] Compile took 3210 milliseconds
[Info - 10:30:19 AM] [ElixirLS Dialyzer] Checking for stale beam files
[Info - 10:30:19 AM] [ElixirLS WorkspaceSymbols] Updating index...
[Info - 10:30:20 AM] [ElixirLS WorkspaceSymbols] 1 modules need reindexing
[Info - 10:30:20 AM] [ElixirLS WorkspaceSymbols] 0 callbacks added to index
[Info - 10:30:20 AM] [ElixirLS WorkspaceSymbols] 0 types added to index
[Info - 10:30:20 AM] [ElixirLS WorkspaceSymbols] 1 modules added to index
[Info - 10:30:20 AM] [ElixirLS WorkspaceSymbols] 57 functions added to index
[Info - 10:30:20 AM] [ElixirLS Dialyzer] Found 41 changed files in 345 milliseconds
[Info - 10:30:20 AM] [ElixirLS Dialyzer] Analyzing 20 modules: [...unrelated modules omitted..., B]
[Info - 10:30:21 AM] [ElixirLS Dialyzer] Analysis finished in 1461 milliseconds
[Info - 10:30:22 AM] Dialyzer analysis is up to date
[Info - 10:30:23 AM] [ElixirLS Dialyzer] Writing manifest...
[Info - 10:30:24 AM] [ElixirLS Dialyzer] Done writing manifest in 1986 milliseconds.
Dialyzer warning
A key of type
'test' cannot exist in a map of type
#{'__meta__' := _,
'__struct__' := 'A',
...all fields except "test"...
}
(No seemingly relevant console messages in the VSCode devtools. Only the usual "starting client" message.)
Heavily abbreviated LSP trace when saving module A
[Trace - 10:42:56 AM] Sending request 'textDocument/formatting - (268)'.
Params: {
"textDocument": {
"uri": "file:///path/to/a.ex"
},
"options": {
"tabSize": 2,
"insertSpaces": true
}
}
[Trace - 10:42:56 AM] Received response 'textDocument/formatting - (268)' in 10ms.
Result: []
[Trace - 10:42:56 AM] Sending notification 'textDocument/didSave'.
Params: {
"textDocument": {
"uri": "file:///path/to/a.ex",
"version": 92
},
"text": ...
}
[omitting traces for emitting log messages shown in the copies above]
[Trace - 10:42:56 AM] Sending notification 'workspace/didChangeWatchedFiles'.
Params: {
"changes": [
{
"uri": "file:///path/to/a.ex",
"type": 2
},
{
"uri": "file:///path/to/a.ex",
"type": 2
}
]
}
[Trace - 10:42:59 AM] Sending notification 'workspace/didChangeWatchedFiles'.
Params: {
"changes": [
{
"uri": "file:///path/to/b.ex",
"type": 2
},
...other files that reference A...
Note: every file appears twice.
]
}
[Trace - 10:43:00 AM] Received notification 'textDocument/publishDiagnostics'.
...for every file in the project that currently has a dialyzer warning, including module B...
[Info - 10:43:01 AM] [ElixirLS Dialyzer] Checking for stale beam files
[Trace - 10:43:01 AM] Received notification 'textDocument/publishDiagnostics'.
...for every file in the project that currently has a dialyzer warning, including module B...
[Info - 10:43:02 AM] [ElixirLS Dialyzer] Analysis finished in 1385 milliseconds
[Trace - 10:43:02 AM] Received notification 'textDocument/publishDiagnostics'.
...for every file in the project that currently has a dialyzer warning, including module B...
...
[Info - 10:43:03 AM] Dialyzer analysis is up to date
[Info - 10:43:04 AM] [ElixirLS Dialyzer] Writing manifest...
[Info - 10:43:05 AM] [ElixirLS Dialyzer] Done writing manifest in 1961 milliseconds.
Heavily abbreviated LSP trace when saving module B
[Trace - 10:59:54 AM] Sending request 'textDocument/formatting - (315)'.
Params: {
"textDocument": {
"uri": "file:///path/to/b.ex"
},
"options": {
"tabSize": 2,
"insertSpaces": true
}
}
[Trace - 10:59:54 AM] Received response 'textDocument/formatting - (315)' in 71ms.
Result: []
[Trace - 10:59:54 AM] Sending notification 'textDocument/didSave'.
Params: {
"textDocument": {
"uri": "file:///path/to/b.ex",
"version": 43
},
"text": ...
}
[omitting traces for emitting log messages shown in the copies above]
MIX_TARGET: host
[Trace - 10:59:54 AM] Sending notification 'workspace/didChangeWatchedFiles'.
Params: {
"changes": [
{
"uri": "file:///path/to/b.ex",
"type": 2
},
{
"uri": "file:///path/to/b.ex",
"type": 2
}
]
}
[Trace - 10:59:57 AM] Received notification 'textDocument/publishDiagnostics'.
...for every file in the project that currently has a dialyzer warning, including module B...
Note: no new warning, yet.
[Info - 10:59:58 AM] [ElixirLS Dialyzer] Found 41 changed files in 366 milliseconds
...
[Info - 11:00:00 AM] [ElixirLS Dialyzer] Analysis finished in 1470 milliseconds
[Trace - 11:00:00 AM] Received notification 'textDocument/publishDiagnostics'.
...for every file in the project that currently has a dialyzer warning, including module B...
Note: includes new warning.
[Info - 11:00:00 AM] Dialyzer analysis is up to date
[Info - 11:00:01 AM] [ElixirLS Dialyzer] Writing manifest...
[Info - 11:00:02 AM] [ElixirLS Dialyzer] Done writing manifest in 2093 milliseconds.
Steps to reproduce:
Anecdotally, I only encounter this issue in larger projects, at least the size of a Phoenix app with a few schemas (which unfortunately I cannot make public).
- Create a module
Awithuse Ecto.Schemaand some number of fields defined. - Create a module
Bwith a function that references%A{}, perhaps pattern matching a value. - Allow dialyzer to write out a manifest.
- In
Aadd a new field to the schema. - In
B, attempt to use the new field, e.g.%A{a | new_field: "value"} - Observe dialyzer warning for the usage of the new field.
Other Notes
- I usually have a canonical type
t :: %__MODULE__{}defined when this occurs. - Manually deleting the BEAM file for module
Ain.elixir_ls/buildand.elixir_ls/dialyzer_tmp, and then reloading the window, does nothing. The BEAM file is not regenerated until another change is made toA. - Deleting
.elixir_lsand reloading, as you might imagine, fixes the issue. - I've experienced this issue over a wide range of Elixir/OTP versions (both the versions running the project, and the versions used to compile ElixirLS).
Expected behavior
Dialyzer should recognize the change to type %A{}.
OTP 26 is going to improve incremental dialyzer
This should help https://github.com/elixir-lsp/elixir-ls/issues/742. Otherwise we can't really fix that
This issue will be resolved on OTP 26+ in the upcoming 0.21 release. This release switches dialyzer backend to builtin OTP incremental mode. Incremental dialyzer mode is much better at tracking dependencies. I tested it and the scenario no longer results in persisting warnings. Unfortunately it is much slower