ExQuality.Finding (ExQuality v0.7.0)
View SourceA single actionable problem reported by a stage.
Stages parse their tool's output into findings so that ExQuality can render
the minimum a reader needs in order to act: a file:line, a message, and the
rule that produced it.
A finding always carries the raw text it was derived from. If a parser is
wrong about a tool's format, the finding degrades into "shows the original
lines" rather than silently dropping a real problem.
Stages that cannot parse their output leave findings empty, and the caller
falls back to printing the tool's full output verbatim. Unparseable output is
never hidden.
Fields
file- path relative to the project rootline- 1-based line number, ornilwhen the tool did not report onecolumn- 1-based column, when the tool reports oneapp- umbrella app the file belongs to, when knownseverity-:error,:warningor:infocheck- the tool's rule identifier (a check module, a category, a code)message- the human-readable problem descriptionraw- the tool output lines this finding was derived from
Summary
Functions
Groups findings by umbrella app, sorted by app name.
Groups findings by file, sorted by file name and by position within a file.
Normalises a path reported by a tool into one relative to the run's root.
Renders findings as a human-readable string, grouped by file.
Sorts findings by file, then line, then column.
Types
@type severity() :: :error | :warning | :info
@type t() :: %ExQuality.Finding{ app: atom() | nil, check: String.t() | nil, column: pos_integer() | nil, file: String.t(), line: pos_integer() | nil, message: String.t(), raw: String.t(), severity: severity() }
Functions
Groups findings by umbrella app, sorted by app name.
Findings with no app come first, as one group under nil, so a single-app
project produces exactly one group.
iex> alias ExQuality.Finding
iex> findings = [
...> %Finding{file: "apps/b/lib/b.ex", app: :b, message: "b"},
...> %Finding{file: "apps/a/lib/a.ex", app: :a, message: "a"}
...> ]
iex> Enum.map(Finding.group_by_app(findings), &elem(&1, 0))
[:a, :b]
Groups findings by file, sorted by file name and by position within a file.
Returns a list of {file, findings} tuples rather than a map, so the order
is stable for rendering.
iex> alias ExQuality.Finding
iex> findings = [
...> %Finding{file: "a.ex", line: 9, message: "later"},
...> %Finding{file: "a.ex", line: 2, message: "earlier"}
...> ]
iex> [{"a.ex", [first, second]}] = Finding.group_by_file(findings)
iex> {first.message, second.message}
{"earlier", "later"}
Normalises a path reported by a tool into one relative to the run's root.
That root is the working directory, which is where mix quality is invoked
from, and which is the project root - the umbrella root, in an umbrella. It
is the same base ExQuality.Umbrella.app_for_path/2 matches against, so a
finding's file and its app cannot disagree about where it is.
Tools disagree about this. Credo reports a relative path, mix_audit reports
an absolute one, and a finding rendered as
/Users/someone/code/app/mix.lock is worse than one rendered as mix.lock
in every place a finding is used: it is longer to read, it does not match
what the reader would type, it differs between a laptop and CI for the same
problem, and it makes two reports of the same finding compare as different.
A path outside the project root is left absolute, because shortening it would be a lie about where the file is. Anything that is not a path - a module name a coverage finding falls back to - passes through unchanged.
iex> ExQuality.Finding.relative_path("mix.lock")
"mix.lock"
iex> ExQuality.Finding.relative_path(Path.join(File.cwd!(), "lib/user.ex"))
"lib/user.ex"
iex> ExQuality.Finding.relative_path(nil)
nil
Renders findings as a human-readable string, grouped by file.
Findings that name an umbrella app are grouped by app first, under a header, because "which app is broken" is the first question a reader of a ten-app umbrella has.
Returns an empty string for an empty list, so callers can test the result and fall back to raw tool output.
iex> alias ExQuality.Finding
iex> finding = %Finding{
...> file: "lib/user.ex", line: 42, column: 3,
...> severity: :warning, check: "readability",
...> message: "Modules should have a @moduledoc tag."
...> }
iex> Finding.render([finding])
"lib/user.ex\n 42:3 [warning] Modules should have a @moduledoc tag. (readability)\n"
Sorts findings by file, then line, then column.
Findings without a line or column sort before those that have one, so a file-level finding leads the file's list.
iex> alias ExQuality.Finding
iex> findings = [
...> %Finding{file: "b.ex", line: 1, message: "second"},
...> %Finding{file: "a.ex", line: 9, message: "first"}
...> ]
iex> Enum.map(Finding.sort(findings), & &1.message)
["first", "second"]