elixir-ls icon indicating copy to clipboard operation
elixir-ls copied to clipboard

Stale Struct Types Lead to Dialyzer Warnings

Open aj-foster opened this issue 3 years ago • 1 comments

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
  • 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 A with use Ecto.Schema and some number of fields defined.
  • Create a module B with a function that references %A{}, perhaps pattern matching a value.
  • Allow dialyzer to write out a manifest.
  • In A add 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 A in .elixir_ls/build and .elixir_ls/dialyzer_tmp, and then reloading the window, does nothing. The BEAM file is not regenerated until another change is made to A.
  • Deleting .elixir_ls and 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{}.

aj-foster avatar Jun 02 '22 15:06 aj-foster

OTP 26 is going to improve incremental dialyzer

lukaszsamson avatar Sep 17 '22 07:09 lukaszsamson

This should help https://github.com/elixir-lsp/elixir-ls/issues/742. Otherwise we can't really fix that

lukaszsamson avatar Oct 06 '22 16:10 lukaszsamson

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

lukaszsamson avatar Mar 15 '24 18:03 lukaszsamson