dialyzer: expand PLT, drop blanket codebase suppression

Ignore file was silencing every warning under lib/towerops/**. Root
cause was plt_add_deps: :apps_direct missing Plug/Phoenix/Oban/Decimal
/Redix/ssl/public_key — hundreds of false `unknown_function` warnings
were being papered over.

Expand plt_add_apps to cover the common deps, narrow the ignore file
to real dep-PLT gaps only, and fix two concrete bugs uncovered by the
change:

- ScopedResource.fetch_preload/4: spec was atom() | [atom()] but
  callers pass keyword lists (e.g. [rules: :targets]).
- ToweropsNative: tag NIF stubs with @dialyzer :nowarn_function so
  dialyzer trusts the @spec instead of the fallback body.

Remaining 328 real warnings surface for follow-up.
This commit is contained in:
Graham McIntire 2026-04-21 09:33:22 -05:00
parent fc51974d36
commit 2653e2516d
4 changed files with 33 additions and 46 deletions

View file

@ -1,69 +1,37 @@
# Dialyzer false positives to ignore
# These are known issues from dependencies with incomplete PLT information
# Dialyzer warnings to ignore.
#
# Most warnings in our codebase are false positives from Ecto, Plug, Oban, and Phoenix
# not exporting their internal types and functions in their PLT files.
#
# We ignore these systematically since they're not real bugs - all tests pass and
# the application runs correctly.
# Dependency PLT gaps should be handled via `plt_add_apps` in mix.exs.
# Only suppress genuine false positives here — fix real warnings instead.
[
# === Dependency behavior warnings ===
# Ecto.Type behavior - callback info not exported
# Ecto.Type behaviour — callbacks aren't always exported
{"deps/ecto/lib/ecto/type.ex", :callback_info_missing},
# Oban.Worker behavior - callback info not exported
# Oban.Worker behaviour — PLT gap
{"deps/oban/lib/oban/worker.ex", :callback_info_missing},
{"deps/oban/lib/oban/worker.ex", :unknown_function},
# Phoenix behaviors - callback info not exported
# Phoenix behaviours
{"deps/phoenix/lib/phoenix/endpoint.ex", :callback_info_missing},
{"deps/phoenix/lib/phoenix/router.ex", :callback_info_missing},
# Plug behaviors - callback info not exported
# Plug behaviours
{"deps/plug/lib/plug/debugger.ex", :unknown_function},
{"deps/plug/lib/plug/error_handler.ex", :unknown_function},
# Honeybadger - unknown function false positives
# Honeybadger
{"deps/honeybadger/lib/honeybadger/plug.ex", :unknown_function},
# Cloak - callback info and unknown function warnings
# Cloak
{"deps/cloak/lib/cloak/vault.ex", :unknown_function},
{"/home/runner/work/elixir/elixir/lib/elixir/lib/gen_server.ex", :callback_info_missing},
{"deps/cloak_ecto/lib/cloak_ecto/type.ex", :callback_info_missing},
{"deps/cloak_ecto/lib/cloak_ecto/types/binary.ex", :unknown_function},
# === Vendored library warnings ===
# SnmpKit vendored library - ignore all warnings (both lib/ and src/)
# Vendored SnmpKit — out of scope for typing cleanup
~r/lib\/snmpkit/,
~r/lib\/snmp_lib/,
~r/src\/snmpkit/,
~r/src\/snmp_lib/,
# === Application code with dependency false positives ===
# Ignore unknown_function and unknown_type warnings from Ecto/Plug/Phoenix
# These are false positives - the functions exist but aren't in the PLT
~r/lib\/towerops.*\.ex/,
~r/lib\/towerops.*\.heex/,
~r/lib\/mix\/tasks.*\.ex/,
# === Gleam type system interop warnings ===
#
# Gleam's type system doesn't map 1:1 to Erlang's type system, causing false positives:
#
# 1. contract_supertype: Gleam only has `Int`, but Dialyzer infers `non_neg_integer()`.
# We cannot make Gleam specs more specific.
#
# 2. call_without_opaque: Gleam stdlib uses opaque types (bytes_tree, Decoder) that
# Dialyzer doesn't understand. The code works correctly at runtime.
#
# 3. no_return/invalid_contract: Dialyzer thinks bytes_tree functions will crash because
# it doesn't understand the opaque type transformations.
#
# All Gleam code compiles and passes tests - these are type system artifacts, not bugs.
# All Gleam files in src/towerops/ (excluding vendored snmpkit above)
# Gleam type system doesn't map 1:1 to Erlang's — artifacts, not bugs.
~r/src\/towerops\/.*\.gleam/
]

View file

@ -22,6 +22,10 @@ defmodule ToweropsNative do
@on_load :load_nif
# NIF stubs return fallback values when the NIF fails to load (defensive).
# Dialyzer only sees the stub bodies, so tell it to trust the @spec instead.
@dialyzer {:nowarn_function, [resolve_oid: 1, init_mib_library: 0, load_mib_directory: 1]}
def load_nif do
nif_path = :filename.join(:code.priv_dir(:towerops), ~c"towerops_nif")

View file

@ -20,7 +20,7 @@ defmodule ToweropsWeb.ScopedResource do
end
end
@spec fetch_preload(module(), Ecto.UUID.t(), Ecto.UUID.t(), atom() | [atom()]) ::
@spec fetch_preload(module(), Ecto.UUID.t(), Ecto.UUID.t(), atom() | list() | keyword()) ::
{:ok, struct()} | {:error, :not_found | :forbidden}
def fetch_preload(schema, id, organization_id, preloads) do
with {:ok, resource} <- fetch(schema, id, organization_id) do

19
mix.exs
View file

@ -105,9 +105,24 @@ defmodule Towerops.MixProject do
defp dialyzer do
[
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
# Faster incremental analysis
plt_add_deps: :apps_direct,
plt_add_apps: [:mix, :ex_unit, :ecto],
plt_add_apps: [
:mix,
:ex_unit,
:ecto,
:plug,
:plug_crypto,
:phoenix,
:phoenix_pubsub,
:phoenix_live_view,
:phoenix_template,
:oban,
:decimal,
:redix,
:telemetry,
:ssl,
:public_key
],
flags: [:unmatched_returns, :error_handling, :underspecs, :unknown],
ignore_warnings: ".dialyzer_ignore.exs"
]