Packages

One-command installer for an Elixir static-analysis suite (credo, dialyxir, ex_dna, ex_slop, boundary, reach) plus a `mix harness.init` whole-codebase audit.

Current section

Files

Jump to
ex_harness lib ex_harness dialyzer_runner.ex
Raw

lib/ex_harness/dialyzer_runner.ex

defmodule ExHarness.DialyzerRunner do
@moduledoc """
Runs `mix dialyzer` in the host project and parses its short-format
output into `[%{file, line, kind, message}]`.
The runner never raises on a non-zero exit code: dialyzer exits with
a non-zero status whenever findings exist, but for ex_harness those
are normal data, not errors. Stderr/exit are captured for the caller.
"""
@type finding :: %{
file: String.t(),
line: integer() | nil,
kind: String.t(),
message: String.t()
}
@type result :: %{
findings: [finding()],
summary: %{total: non_neg_integer()},
exit_status: integer(),
stderr: String.t() | nil
}
@doc """
Run dialyzer in `root` and return parsed findings.
Options:
* `:quick` — when `true`, skip `mix dialyzer --plt` rebuild and
reuse whatever PLT already exists. Defaults to `false`.
* `:env` — extra env vars passed to `System.cmd/3`.
"""
@spec run(String.t(), keyword()) :: result()
def run(root, opts \\ []) do
if not opts[:quick] do
_ = System.cmd("mix", ["dialyzer", "--plt"], cd: root, stderr_to_stdout: true)
end
{out, status} =
System.cmd("mix", ["dialyzer", "--format", "short", "--quiet"],
cd: root,
stderr_to_stdout: true
)
findings = parse(out)
%{
findings: findings,
summary: %{total: length(findings)},
exit_status: status,
stderr: nil
}
end
@doc """
Parse dialyzer's `--format short` output into findings.
"""
@spec parse(String.t()) :: [finding()]
def parse(output) when is_binary(output) do
output
|> String.split("\n", trim: true)
|> Enum.flat_map(&parse_line/1)
end
defp parse_line(line) do
# Short format example:
# lib/foo.ex:42:no_return Function bar/1 has no local return
case Regex.run(~r/^([^:]+):(\d+):([^\s]+)\s+(.+)$/, line) do
[_, file, line_str, kind, message] ->
[
%{
file: file,
line: parse_int(line_str),
kind: kind,
message: message
}
]
_ ->
[]
end
end
defp parse_int(str) do
case Integer.parse(str) do
{n, _} -> n
:error -> nil
end
end
end