Powered by AppSignal & Oban Pro

Dialyzer Error Debugger

livebooks/dialyzer_debugger.livemd

Dialyzer Error Debugger

Mix.install([
  {:kino, "~> 0.12"},
  {:kino_vega_lite, "~> 0.1"},
  {:jason, "~> 1.4"}
])

Introduction

This Livebook helps us systematically work through all Dialyzer errors in the project.

Integration with Mix Task:

  • This Livebook loads data from dialyzer_reports/dialyzer_status.json
  • Run mix dialyzer.analyze to generate/update the tracker
  • Changes made here can be saved back to the tracker file

Load Data

# Change to project directory
project_dir = "/home/chops/src/elixir-phoenix"
File.cd!(project_dir)

tracker_file = Path.join(project_dir, "dialyzer_reports/dialyzer_status.json")

# Load existing tracker or run analysis
tracker = case File.read(tracker_file) do
  {:ok, content} ->
    Jason.decode!(content, keys: :atoms)
  {:error, _} ->
    Kino.Markdown.new("""
    ⚠️ **No tracker file found!**

    Run this first:
    ```bash
    mix dialyzer.analyze
    ```
    """) |> Kino.render()

    %{errors: [], last_run: nil, total_count: 0, status_counts: %{new: 0, investigated: 0, fixed: 0}}
end

errors = tracker[:errors] || []

Kino.Markdown.new("""
## Loaded Tracker Data

**Last Run:** #{tracker[:last_run] || "Never"}
**Total Errors:** #{tracker[:total_count]}

**Status Breakdown:**
- πŸ†• New: #{tracker[:status_counts][:new] || 0}
- πŸ” Investigated: #{tracker[:status_counts][:investigated] || 0}
- βœ… Fixed: #{tracker[:status_counts][:fixed] || 0}
""")
# UI for marking errors
mark_input = Kino.Input.text("Error IDs to mark (comma-separated)")
status_input = Kino.Input.select("Status", [
  {"New", :new},
  {"Investigated", :investigated},
  {"Fixed", :fixed}
])
mark_button = Kino.Control.button("Mark Errors")

Kino.Layout.grid([mark_input, status_input, mark_button], columns: 3)
# Handle marking
Kino.listen(mark_button, fn _event ->
  ids_str = Kino.Input.read(mark_input)
  status = Kino.Input.read(status_input)

  if ids_str != "" do
    ids = ids_str
    |> String.split(",")
    |> Enum.map(&String.trim/1)
    |> Enum.map(&String.to_integer/1)

    # Update tracker
    updated_tracker = %{tracker |
      errors: Enum.map(tracker.errors, fn error ->
        if error.id in ids do
          Map.put(error, :status, status)
        else
          error
        end
      end)
    }

    # Recalculate status counts
    status_counts = updated_tracker.errors
    |> Enum.group_by(& &1.status)
    |> Map.new(fn {s, errs} -> {s, length(errs)} end)
    |> Map.put_new(:new, 0)
    |> Map.put_new(:investigated, 0)
    |> Map.put_new(:fixed, 0)

    updated_tracker = Map.put(updated_tracker, :status_counts, status_counts)

    # Save tracker
    File.write!(tracker_file, Jason.encode!(updated_tracker, pretty: true))

    Kino.Markdown.new("""
    βœ… **Updated #{length(ids)} errors to status: #{status}**

    Re-evaluate the "Load Data" cell to see changes.
    """) |> Kino.render()
  else
    Kino.Markdown.new("❌ Please enter error IDs") |> Kino.render()
  end
end)

Kino.nothing()
# Group by error type
by_type = Enum.group_by(errors, & &1.type)

type_data = by_type
|> Enum.map(fn {type, errs} ->
  %{type: type, count: length(errs)}
end)
|> Enum.sort_by(& &1.count, :desc)

VegaLite.new(width: 600, height: 300, title: "Errors by Type")
|> VegaLite.data_from_values(type_data)
|> VegaLite.mark(:bar)
|> VegaLite.encode_field(:x, "type", type: :nominal, title: "Error Type")
|> VegaLite.encode_field(:y, "count", type: :quantitative, title: "Count")
# Group by file
by_file = Enum.group_by(errors, & &1.file)

file_data = by_file
|> Enum.map(fn {file, errs} ->
  %{file: Path.basename(file), count: length(errs), location: hd(errs).location}
end)
|> Enum.sort_by(& &1.count, :desc)

VegaLite.new(width: 600, height: 400, title: "Errors by File")
|> VegaLite.data_from_values(file_data)
|> VegaLite.mark(:bar)
|> VegaLite.encode_field(:x, "file", type: :nominal, title: "File")
|> VegaLite.encode_field(:y, "count", type: :quantitative, title: "Count")
|> VegaLite.encode_field(:color, "location", type: :nominal,
    scale: %{"range" => ["#e74c3c", "#3498db", "#95a5a6"]})
# Create a filterable table of errors
error_table_data = errors
|> Enum.with_index(1)
|> Enum.map(fn {error, idx} ->
  %{
    "#" => idx,
    "Type" => error.type,
    "File" => Path.basename(error.file),
    "Line" => error.line,
    "Location" => error.location,
    "Agent" => error.agent || "N/A"
  }
end)

Kino.DataTable.new(error_table_data, name: "All Dialyzer Errors")
# Select an error to inspect
error_selector = Kino.Input.select(
  "Select error to inspect:",
  errors
  |> Enum.with_index(1)
  |> Enum.map(fn {err, idx} ->
    {"##{idx}: #{Path.basename(err.file)}:#{err.line} (#{err.type})", idx - 1}
  end)
)
selected_value = Kino.Input.read(error_selector)

# Handle case where Kino returns the label instead of value
selected_idx = if is_integer(selected_value) do
  selected_value
else
  # Try to extract number from string like "#32: ..."
  case Regex.run(~r/^#(\d+):/, to_string(selected_value)) do
    [_, num_str] -> String.to_integer(num_str) - 1
    _ -> nil
  end
end

selected_error = if selected_idx, do: Enum.at(errors, selected_idx), else: nil

if selected_error do
  markdown = """
  ## Error ##{selected_idx + 1}

  **File:** #{selected_error.file}
  **Line:** #{selected_error.line}
  **Type:** #{selected_error.type}
  **Location:** #{selected_error.location}
  **Agent:** #{selected_error.agent || "N/A"}

  ### Full Error Text

  """ <> "```\n#{selected_error.full_text}\n```\n\n### Source Code Context\n"

  Kino.Markdown.new(markdown)
else
  Kino.Markdown.new("No error selected")
end
# Show source code around the error
if selected_error do
  file_path = Path.join(project_dir, selected_error.file)

  if File.exists?(file_path) do
    lines = File.read!(file_path) |> String.split("\n")

    # Show 10 lines before and after
    start_line = max(0, selected_error.line - 10)
    end_line = min(length(lines), selected_error.line + 10)

    context = lines
    |> Enum.slice(start_line..end_line)
    |> Enum.with_index(start_line + 1)
    |> Enum.map(fn {line, num} ->
      marker = if num == selected_error.line, do: ">>> ", else: "    "
      "#{marker}#{num}: #{line}"
    end)
    |> Enum.join("\n")

    Kino.Markdown.new("```elixir\n#{context}\n```")
  else
    Kino.Markdown.new("*File not found: #{file_path}*")
  end
else
  Kino.Markdown.new("")
end
# Group errors by category
categories = %{
  unused_functions: errors |> Enum.filter(&amp;(&amp;1.type == "unused_fun")),
  contract_violations: errors |> Enum.filter(&amp;(&amp;1.type == "call")),
  invalid_contracts: errors |> Enum.filter(&amp;(&amp;1.type == "invalid_contract")),
  pattern_matches: errors |> Enum.filter(&amp;(&amp;1.type == "pattern_match")),
  guard_fails: errors |> Enum.filter(&amp;(&amp;1.type == "guard_fail"))
}

Kino.Markdown.new("""
## Error Categories

### 1. Unused Functions (#{length(categories.unused_functions)})
Functions that Dialyzer believes will never be called.

### 2. Contract Violations (#{length(categories.contract_violations)})
Function calls that don't match the declared @spec.

### 3. Invalid Contracts (#{length(categories.invalid_contracts)})
@spec declarations that don't match the actual function implementation.

### 4. Pattern Matches (#{length(categories.pattern_matches)})
Patterns that can never match based on type analysis.

### 5. Guard Failures (#{length(categories.guard_fails)})
Guard clauses that can never succeed.
""")
# Separate framework and our code errors
framework_errors = Enum.filter(errors, &amp;(&amp;1.location == :jido_framework))
our_errors = Enum.filter(errors, &amp;(&amp;1.location == :our_code))

Kino.Layout.tabs([
  {"Jido Framework (#{length(framework_errors)})",
    Kino.DataTable.new(
      framework_errors
      |> Enum.map(&amp;%{
        "Type" => &amp;1.type,
        "Line" => &amp;1.line,
        "Agent" => &amp;1.agent || "N/A"
      }),
      name: "Framework Errors"
    )
  },
  {"Our Code (#{length(our_errors)})",
    Kino.DataTable.new(
      our_errors
      |> Enum.map(&amp;%{
        "File" => Path.basename(&amp;1.file),
        "Type" => &amp;1.type,
        "Line" => &amp;1.line,
        "Agent" => &amp;1.agent || "N/A"
      }),
      name: "Our Code Errors"
    )
  }
])
Kino.Markdown.new("""
## Next Steps

Based on the analysis above, here's what we can do:

### Errors We Can Fix

1. **Pattern Match Errors** - These may indicate real bugs in our code
2. **Invalid Contracts in Our Code** - We can add proper @dialyzer directives

### Errors That Are Framework Issues

1. **Unused Functions** - These are in the Jido framework macro system
2. **Contract Violations** - The `use Jido.Agent` macro generates code with mismatched specs

### Decision Points

- Can we add @dialyzer {:nowarn_function, ...} to our agent modules?
- Should we override the generated specs?
- Do we need to modify how we use `use Jido.Agent`?
""")
refresh_button = Kino.Control.button("πŸ”„ Re-run Dialyzer Analysis")
Kino.listen(refresh_button, fn _event ->
  Kino.Markdown.new("Running `mix dialyzer.analyze`...") |> Kino.render()

  {output, exit_code} = System.cmd("mix", ["dialyzer.analyze"],
    stderr_to_stdout: true,
    env: [{"MIX_ENV", "dev"}]
  )

  Kino.Markdown.new("""
  ## βœ… Analysis Complete

  **Exit Code:** #{exit_code}

  ### Output:

#{String.slice(output, 0..1000)}


  **Re-evaluate the "Load Data" cell to see updated data.**
  """) |> Kino.render()
end)

Kino.nothing()
Kino.Markdown.new("""
## Terminal Commands

Run these in your terminal:

mix dialyzer.analyze

open dialyzer_reports/dialyzer_report.html

mix dialyzer.analyze –mark-investigated 1,5,12

mix dialyzer.analyze –mark-fixed 3,7

""")