Structured findings from running analyses in-process.
run/2 (exposed as Argus.run_analyses/2) extracts facts once, evaluates
each selected analysis's Datalog rules against the shared facts directory,
and converts every output-relation row into a finding map with a severity,
human-readable prose, and the most precise anchor the row allows:
instr— anArgus.InstrIdwhen the row carries an instruction ID.mfa—{module, function, arity}when the row carries a function ID (or names a well-known callback such asinit/1).module— always set when the row names a module at all.
Degradation
The promise is degrade-, never crash-, and never silently:
- Souffle missing from PATH →
{:error, :souffle_not_found}up front. - Fact extraction failing (unknown module, unreadable
.beam) →{:error, reason}— nothing could have run. - A single analysis erroring (rules bug, Souffle timeout) → a
degradedentry naming the analysis and why, while every other analysis still runs and reports.
Atom creation
Anchor parsing converts module and function name strings back to atoms
with String.to_atom/1. Those names come from BEAM files the caller
asked Argus to disassemble, so the atoms already exist in this node's
atom table — parsing does not grow it. Do not feed findings from
untrusted .beam files into a long-lived node; run Argus in a sandbox
process instead (this is how lowdown consumes uploads).
Summary
Types
Code location attached to a finding, most precise field wins.
What an analysis module's finding/2 callback returns.
Per-analysis degradation note for analyses that did not complete.
A finding: attrs plus the analysis that produced it.
Per-analysis run record for analyses that completed.
A labelled secondary location (sibling, supervisor, callee, ...).
Finding severity, in decreasing order of urgency.
Functions
Anchor for a function ID string ("Mod:func/arity").
Anchor for an instruction ID string ("Mod:func/arity#idx").
Anchor for a known callback on a module string.
Anchor for a module string ("MyApp.Cache" or ":lists").
Anchor for a site ID of either precision, falling back to a module.
Deduplicates a relation's rows down to one per logical finding.
Converts an inspect/1-rendered module string back to the module atom.
Builds finding attributes.
Labels an anchor as a secondary location.
Runs analyses against the given modules and returns structured findings.
Types
@type anchor() :: %{ module: module() | nil, mfa: mfa() | nil, instr: Argus.InstrId.t() | nil }
Code location attached to a finding, most precise field wins.
@type attrs() :: %{ severity: severity(), title: String.t(), detail: String.t(), module: module() | nil, mfa: mfa() | nil, instr: Argus.InstrId.t() | nil, at_label: String.t() | nil, help: [String.t()], related: [related()] }
What an analysis module's finding/2 callback returns.
Per-analysis degradation note for analyses that did not complete.
@type finding() :: %{ analysis: atom(), severity: severity(), title: String.t(), detail: String.t(), module: module() | nil, mfa: mfa() | nil, instr: Argus.InstrId.t() | nil, at_label: String.t() | nil, help: [String.t()], related: [related()] }
A finding: attrs plus the analysis that produced it.
@type ran_entry() :: %{ analysis: atom(), duration_ms: non_neg_integer(), finding_count: non_neg_integer() }
Per-analysis run record for analyses that completed.
@type severity() :: :error | :warning | :info
Finding severity, in decreasing order of urgency.
@type t() :: %Argus.Findings{ degraded: [degradation()], findings: [finding()], ran: [ran_entry()] }
Functions
Anchor for a function ID string ("Mod:func/arity").
Anchor for an instruction ID string ("Mod:func/arity#idx").
Unparseable input (a "dynamic" placeholder, free-form text) yields an
empty anchor rather than an error — anchors are best-effort by design.
Anchor for a known callback on a module string.
Several relations report a module known to implement a specific callback
(init/1, handle_cast/2, ...) without carrying a function ID — this
reconstructs the precise anchor.
Anchor for a module string ("MyApp.Cache" or ":lists").
Anchor for a site ID of either precision, falling back to a module.
Witness columns hold an instruction ID where the extractor had one and
a function ID otherwise; extractors mark sites they cannot resolve
with a "dynamic" placeholder. This tries the most precise parse
first — instruction, then function, then the module fallback — so a
finding never loses its module anchor to an unresolvable site.
@spec dedupe_rows(Argus.Analysis.output_relation(), [[String.t()]]) :: [[String.t()]]
Deduplicates a relation's rows down to one per logical finding.
Relations with witness columns yield one row per witnessing site; rows
that agree on the relation's declared :key fields describe the same
finding. Keeps the lexicographically least row of each group — a
deterministic representative, so finding counts and anchors never
depend on Souffle's row order or on how many sites witness the same
defect. Relations without a :key pass through unchanged.
Public because in-process embedders that build findings themselves
(the planchette pattern) must apply the same identity rule or their
counts drift from run/2's.
Converts an inspect/1-rendered module string back to the module atom.
Returns nil for the "dynamic" placeholder and anything else that
isn't a module rendering.
Builds finding attributes.
opts:
:at— an anchor fromat_instr/1,at_func/1,at_module/1, orat_mfa/3(default: no anchor).:at_label— what the anchor line IS, for renderers that excerpt the source ("supervision tree defined here"), so the annotation does not just repeat the title (default:nil).:help— resolution guidance, one string per suggestion, rendered by consumers as help trailers. Say what to change and toward what, in the row's own terms (default:[]).:related— list ofrelated/2entries (default:[]).
Runs analyses against the given modules and returns structured findings.
modules is a list of module atoms or paths to .beam files, exactly
as Argus.analyze/3 accepts.
Options
:analyses—:all(default) or a list of built-in analysis names.:allmeans every built-in analysis except:coverage, which measures the extractor pipeline rather than the analyzed code.:facts_dir— a directoryArgus.Analysis.extract_facts/3already wrote for these modules, to evaluate without extracting again. The caller owns it; without this option the run extracts into a temporary directory and removes it afterwards.:concurrency— parallel Souffle solves (default: the scheduler count, capped at 4; each solve holds its own copy of the call graph's closure). Extraction always runs at scheduler width.- All other
Argus.Analysis.run/3options (:extractors,:souffle_bin,:souffle_timeout, ...) pass through.
Returns {:ok, %Argus.Findings{}} or {:error, reason} — see the
moduledoc for the degradation contract.