Packages

AI-friendly JSON output for Dialyzer warnings. Structured output for Claude Code and similar AI editors.

Current section

4 Versions

Jump to

Compare versions

5 files changed
+102 additions
-9 deletions
  @@ -0,0 +1 @@
1 + 0.2.0
  @@ -4,6 +4,24 @@ Completed roadmap tasks. For upcoming work, see [ROADMAP.md](ROADMAP.md).
4 4
5 5 ---
6 6
7 + ## [0.2.0] - 2026-03-21
8 +
9 + ### Added
10 + - `.dialyzer_ignore.exs` support — warnings matching the project's ignore file are now filtered out, matching dialyxir's behavior
11 + - `skipped` field in summary output showing how many warnings were filtered by the ignore file
12 + - Respects dialyxir's `dialyzer_flags` and `dialyzer_removed_defaults` configuration for warning flags
13 +
14 + ### Changed
15 + - Warning flags now match dialyxir's logic exactly instead of hardcoding `[:unknown]`
16 + - Added tidewave alias for local MCP development (port 4002)
17 +
18 + ## [0.1.1] - 2026-02-02
19 +
20 + ### Added
21 + - Initial hex.pm release with full feature set (see Phase 1-5 below)
22 +
23 + ---
24 +
7 25 ## Phase 1: Core Foundation
8 26
9 27 ### Initial Implementation
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/ZenHive/dialyzer_json">>}]}.
2 2 {<<"name">>,<<"dialyzer_json">>}.
3 - {<<"version">>,<<"0.1.1">>}.
3 + {<<"version">>,<<"0.2.0">>}.
4 4 {<<"description">>,
5 5 <<"AI-friendly JSON output for Dialyzer warnings. Structured output for Claude Code and similar AI editors.">>}.
6 6 {<<"elixir">>,<<"~> 1.19">>}.
  @@ -11,7 +11,8 @@
11 11 <<"lib/mix/tasks/dialyzer_json.ex">>,<<"lib/dialyzer_json">>,
12 12 <<"lib/dialyzer_json/warning_encoder.ex">>,
13 13 <<"lib/dialyzer_json/fix_hint.ex">>,<<".formatter.exs">>,<<"mix.exs">>,
14 - <<"README.md">>,<<"CHANGELOG.md">>,<<"LICENSE">>,<<"AGENTS.md">>]}.
14 + <<".version">>,<<"README.md">>,<<"CHANGELOG.md">>,<<"LICENSE">>,
15 + <<"AGENTS.md">>]}.
15 16 {<<"requirements">>,
16 17 [[{<<"name">>,<<"jason">>},
17 18 {<<"app">>,<<"jason">>},
  @@ -54,6 +54,7 @@ defmodule Mix.Tasks.Dialyzer.Json do
54 54 "warnings": [...],
55 55 "summary": {
56 56 "total": 5,
57 + "skipped": 0,
57 58 "by_type": {"no_return": 2, "call": 3},
58 59 "by_fix_hint": {"code": 4, "spec": 1}
59 60 }
  @@ -154,10 +155,14 @@ defmodule Mix.Tasks.Dialyzer.Json do
154 155 ensure_plt!()
155 156
156 157 # Run dialyzer and get warnings
157 - warnings = run_dialyzer()
158 + raw_warnings = run_dialyzer()
159 +
160 + # Filter ignored warnings (.dialyzer_ignore.exs)
161 + filter_map = load_filter_map()
162 + {warnings, skipped} = apply_ignore_filter(raw_warnings, filter_map)
158 163
159 164 # Encode to JSON
160 - encoded = encode_output(warnings, opts)
165 + encoded = encode_output(warnings, Keyword.put(opts, :skipped, skipped))
161 166
162 167 # Output
163 168 output_json(encoded, opts)
  @@ -252,20 +257,76 @@ defmodule Mix.Tasks.Dialyzer.Json do
252 257 :ok
253 258 end
254 259
260 + @doc false
261 + # Filters raw warnings against the project's ignore file (.dialyzer_ignore.exs).
262 + # Returns {kept_warnings, skipped_count}.
263 + @spec apply_ignore_filter([WarningEncoder.warning()], struct()) ::
264 + {[WarningEncoder.warning()], non_neg_integer()}
265 + def apply_ignore_filter(warnings, filter_map) do
266 + # credo:disable-for-next-line Credo.Check.Refactor.Apply
267 + if apply(Dialyxir.FilterMap, :filters, [filter_map]) == [] do
268 + {warnings, 0}
269 + else
270 + {kept, skipped} =
271 + Enum.reduce(warnings, {[], 0}, &partition_warning(&1, &2, filter_map))
272 +
273 + {Enum.reverse(kept), skipped}
274 + end
275 + end
276 +
277 + @doc false
278 + # Checks a single warning against the filter map and partitions into kept/skipped
279 + @spec partition_warning(
280 + WarningEncoder.warning(),
281 + {[WarningEncoder.warning()], non_neg_integer()},
282 + struct()
283 + ) ::
284 + {[WarningEncoder.warning()], non_neg_integer()}
285 + defp partition_warning(warning, {kept_acc, skipped_acc}, filter_map) do
286 + # credo:disable-for-next-line Credo.Check.Refactor.Apply
287 + {should_skip, _matching} = apply(Dialyxir.Project, :filter_warning?, [warning, filter_map])
288 +
289 + if should_skip do
290 + {kept_acc, skipped_acc + 1}
291 + else
292 + {[warning | kept_acc], skipped_acc}
293 + end
294 + end
295 +
296 + @doc false
297 + # Loads the FilterMap from the project's ignore_warnings config
298 + @spec load_filter_map() :: struct()
299 + defp load_filter_map do
300 + # credo:disable-for-next-line Credo.Check.Refactor.Apply
301 + apply(Dialyxir.Project, :filter_map, [[]])
302 + end
303 +
304 + @doc false
305 + # Computes warning flags matching dialyxir's logic:
306 + # transform(raw_opts) ++ (@default_warnings -- removed_defaults)
307 + @spec build_warning_flags([atom()], [atom()]) :: [atom()]
308 + def build_warning_flags(flags, removed_defaults) do
309 + flags ++ ([:unknown] -- removed_defaults)
310 + end
311 +
255 312 @doc false
256 313 # Runs dialyzer and returns the list of warnings
257 314 @spec run_dialyzer() :: [WarningEncoder.warning()]
258 315 defp run_dialyzer do
259 316 # Runtime calls to avoid compile-time warnings when used as a path dependency
260 - # credo:disable-for-lines:2 Credo.Check.Refactor.Apply
317 + # credo:disable-for-lines:4 Credo.Check.Refactor.Apply
261 318 plt_file = apply(Dialyxir.Project, :plt_file, [])
262 319 files = apply(Dialyxir.Project, :dialyzer_files, [])
320 + flags = apply(Dialyxir.Project, :dialyzer_flags, [])
321 + removed_defaults = apply(Dialyxir.Project, :dialyzer_removed_defaults, [])
322 +
323 + warnings = build_warning_flags(flags, removed_defaults)
263 324
264 325 args = [
265 326 check_plt: false,
266 327 init_plt: String.to_charlist(plt_file),
267 328 files: files,
268 - warnings: [:unknown]
329 + warnings: warnings
269 330 ]
270 331
271 332 try do
  @@ -288,6 +349,7 @@ defmodule Mix.Tasks.Dialyzer.Json do
288 349
289 350 summary = %{
290 351 total: length(encoded_warnings),
352 + skipped: Keyword.get(opts, :skipped, 0),
291 353 by_type: count_by_type(encoded_warnings),
292 354 by_fix_hint: count_by_fix_hint(encoded_warnings)
293 355 }
  @@ -2,18 +2,20 @@ defmodule DialyzerJson.MixProject do
2 2 use Mix.Project
3 3
4 4 @source_url "https://github.com/ZenHive/dialyzer_json"
5 + @version ".version" |> File.read!() |> String.trim()
5 6
6 7 def project do
7 8 [
8 9 app: :dialyzer_json,
9 - version: "0.1.1",
10 + version: @version,
10 11 elixir: "~> 1.19",
11 12 start_permanent: Mix.env() == :prod,
12 13 deps: deps(),
13 14 dialyzer: dialyzer(),
14 15 description: description(),
15 16 package: package(),
16 - source_url: @source_url
17 + source_url: @source_url,
18 + aliases: aliases()
17 19 ]
18 20 end
19 21
  @@ -35,7 +37,7 @@ defmodule DialyzerJson.MixProject do
35 37 [
36 38 licenses: ["MIT"],
37 39 links: %{"GitHub" => @source_url},
38 - files: ~w(lib .formatter.exs mix.exs README.md CHANGELOG.md LICENSE AGENTS.md)
40 + files: ~w(lib .formatter.exs mix.exs .version README.md CHANGELOG.md LICENSE AGENTS.md)
39 41 ]
40 42 end
41 43
  @@ -46,6 +48,15 @@ defmodule DialyzerJson.MixProject do
46 48 ]
47 49 end
48 50
51 + defp aliases do
52 + [
53 + # Port 4002 to avoid conflict with consumer projects on 4001
54 + tidewave: [
55 + "run --no-halt -e 'Agent.start(fn -> Bandit.start_link(plug: Tidewave, port: 4002) end)'"
56 + ]
57 + ]
58 + end
59 +
49 60 # Run "mix help deps" to learn about dependencies.
50 61 defp deps do
51 62 [