diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 0000000..c89978c --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1,11 @@ +# Dialyzer ignore list. +# +# This file is for PROVEN false positives only. Each entry must carry a +# `# why:` comment explaining why Dialyzer is wrong about the call site. +# Real findings get fixed by adjusting @spec, return types, or pattern +# matches — never silenced here. +# +# Format: each entry is either a path string, a {path, warning} tuple, +# or a {path, warning, line} tuple. See: +# https://hexdocs.pm/dialyxir/readme.html#elixir-format +[] diff --git a/.drone.jsonnet b/.drone.jsonnet index ba8bacd..388e8f4 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -27,7 +27,7 @@ local step_compute_cache = { local step_restore_cache = { name: 'restore-cache', image: 'drillster/drone-volume-cache', - settings: { restore: true, mount: ['./deps', './_build'], ttl: 30 }, + settings: { restore: true, mount: ['./deps', './_build', './priv/plts'], ttl: 30 }, volumes: cache_mount, }; @@ -47,6 +47,20 @@ local step_lint = { ], }; +local step_typecheck = { + name: 'typecheck', + image: elixir, + commands: [ + 'mix local.hex --force', + 'mix deps.get', + 'mkdir -p priv/plts', + // Build/refresh PLT — no-op on cache hit, full build (5-15 min) on cache miss. + 'mix dialyzer --plt', + // Actual typecheck. --format short keeps log noise down on red builds. + 'mix dialyzer --format short', + ], +}; + local step_wait_postgres = { name: 'wait_for_postgres', image: postgres_image, @@ -69,7 +83,7 @@ local step_wait_postgres = { local step_rebuild_cache = { name: 'rebuild-cache', image: 'drillster/drone-volume-cache', - settings: { rebuild: true, mount: ['./deps', './_build'] }, + settings: { rebuild: true, mount: ['./deps', './_build', './priv/plts'] }, volumes: cache_mount, }; @@ -99,6 +113,7 @@ local check_pipeline(name, trigger, test) = { step_compute_cache, step_restore_cache, step_lint, + ] + (if test.name == 'test-all' then [step_typecheck] else []) + [ step_wait_postgres, test, step_rebuild_cache, diff --git a/.gitignore b/.gitignore index b9096bd..14620df 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ notes.md # Do NOT commit these — they are local to the dev machine .pipeline/ .claude/ + +# Dialyzer PLT files — built locally and in CI cache, never tracked. +/priv/plts/*.plt +/priv/plts/*.plt.hash diff --git a/Justfile b/Justfile index e0bd0d3..54c395f 100644 --- a/Justfile +++ b/Justfile @@ -31,6 +31,21 @@ start-database: ci-dev: lint audit test-fast +# Build the Dialyzer PLT. Idempotent — no-op once the PLT is up to date. +# First build takes 5–15 min; subsequent runs are seconds. PLT files live in +# priv/plts/ and are gitignored. +plt: install-dependencies + @mkdir -p priv/plts + mix dialyzer --plt + +# Typecheck via Dialyzer. Slow stage, NOT part of ci-dev. +typecheck: plt + mix dialyzer --format short + +# Full CI: inner loop plus typecheck. Use locally before pushing; Drone CI +# runs equivalent steps with PLT caching. +ci: ci-dev typecheck + gettext: mix gettext.extract mix gettext.merge priv/gettext --on-obsolete=mark_as_obsolete diff --git a/mix.exs b/mix.exs index c037e46..a510a7e 100644 --- a/mix.exs +++ b/mix.exs @@ -12,6 +12,7 @@ defmodule Mv.MixProject do compilers: [:phoenix_live_view] ++ Mix.compilers(), aliases: aliases(), deps: deps(), + dialyzer: dialyzer(), listeners: [Phoenix.CodeReloader], gettext: [write_reference_line_numbers: false] ] @@ -80,6 +81,7 @@ defmodule Mv.MixProject do {:sobelow, "~> 0.14", only: [:dev, :test], runtime: false}, {:bypass, "~> 2.1", only: [:dev, :test]}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:picosat_elixir, "~> 0.1"}, {:ecto_commons, "~> 0.3"}, {:slugify, "~> 1.3"}, @@ -112,4 +114,21 @@ defmodule Mv.MixProject do "phx.routes": ["phx.routes", "ash_authentication.phoenix.routes"] ] end + + defp dialyzer do + [ + plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, + plt_core_path: "priv/plts/core.plt", + plt_add_apps: [:mix, :ex_unit], + flags: [ + :error_handling, + :unmatched_returns, + :extra_return, + :missing_return, + :underspecs + ], + ignore_warnings: ".dialyzer_ignore.exs", + list_unused_filters: true + ] + end end diff --git a/mix.lock b/mix.lock index 0a36e9e..7dd592f 100644 --- a/mix.lock +++ b/mix.lock @@ -23,11 +23,13 @@ "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, "db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"}, "decimal": {:hex, :decimal, "3.1.0", "9ede268cff827e6f0c4fb1b34747c82630dce5d7b877dfb22ec8f0cb25855fce", [:mix], [], "hexpm", "e8b3efb3bb3a13cb5e4268ffe128569067b1972e9dee013537c71a5b073168f9"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "ecto": {:hex, :ecto, "3.13.6", "352135b474f91d1ab99a1b502171d207e9db60421c9e3d0ecab4c7ab96b24d14", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8afa059bc16cd2c94739ec0a11e3e5df69d828125119109bef35f20a21a76af2"}, "ecto_commons": {:hex, :ecto_commons, "0.3.7", "f33c162a6f63695d5939af02c65a0e76aa6e7278b82c7bfc357ffbfea353bf0f", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.4", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "9c33771ebd38cd83d3f90fab6069826ba9d4f7580f1481b3c0913f8b9795c5fd"}, "ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_phone_number": {:hex, :ex_phone_number, "0.4.10", "11809f6600b2ecb0a2e75d496c2ec2f273d49d1e2f58b2be2667decb0aabfb43", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "eefccf58d8149d64af658721bff0edcb9e9b8943f74000ede151948ef03046c1"},