pr-miner
Mines implicit programming rules via Apriori frequent-itemset
analysis over per-function call-site transactions, then flags
function bodies that violate the mined rule. The contradiction is
between a statistically-supported pairing (e.g. lock → unlock)
and a single function that calls one without the other.
| Property | Value |
|---|---|
| Detector ID | pr-miner |
| Languages | Rust, Python |
| IEEE 1044-2009 class | Logic |
| Default severity | Warning |
| Spec | docs/spec/pr-miner-v0.md |
How it works
- Each top-level function in the scan becomes one transaction whose items are the distinct call-site names inside its body.
- Apriori mines association rules
{a} → {b}withsupport ≥ MIN_SUPPORT = 0.05andconfidence ≥ MIN_CONFIDENCE = 0.85. - Any function whose body contains
abut neverbis a violator and receives one Finding. Finding.relatedlists every function in the database that satisfies the rule (capped atMAX_RELATED = 32).
MAX_ITEMSET_SIZE = 2 keeps the algorithm to pairs in v0. Lifting
to FP-growth and N > 2 itemsets is tracked under Future Q-series
candidates in the roadmap.
What it flags
#![allow(unused)]
fn main() {
fn handle_request(conn: &Mutex<Conn>) -> Result<()> {
let guard = lock(conn);
process(&guard);
// <- flagged: 9 of 10 functions that call lock(...) also call unlock(...)
Ok(())
}
}
The message format is:
function calls lock but never unlock; 9 of 10 similar functions (90%) call both
The N / M / percent figures come straight from the rule’s
support and confidence numbers; the evidence.raw payload carries
the full rule_lhs, rule_rhs, support, confidence,
transaction_count, and related_capped fields so a reviewer can
audit the inference without re-running the miner.
What it does not flag
- Closures, dynamic dispatch (
obj[i]()), and other non-identifier call heads. The extractor only seescall_expression/callnodes whose head is apath/identifier/attribute. - Functions with fewer than
MIN_TRANSACTION_ITEMS = 2distinct call items. Single-call bodies have no signal. - Any rule when the transaction database is smaller than
MIN_DATABASE_SIZE = 20. Apriori on tiny corpora is too noisy to act on. - Inter-procedural rules (lock acquired in
f, released ing).
Language coverage
- Rust:
li-zhou-fse-2005is grandfathered as a confirmed citation perdocs/spec/citations-policy.md. - Python: unconfirmed citation per
docs/surveys/pr-miner-python-2026-05.md. P1 is satisfied by the Rust citation as a cross-language extension.
Empirical FP profile
The v0.1 calibration labels 16 TP / 22 FP across the seed and wild
corpora (posterior_tp ≈ 0.43). The dominant failure mode (21 / 22
FPs) is stdlib-constructor co-occurrence: Err(...) and Ok(...)
both reduce to the items Err / Ok under the last-segment rule,
so Apriori mines them as a paired API even though no contract is
being violated. The spec’s “v1 mitigations under consideration”
section discusses three tightening options (stop-list, fully-
qualified paths, cardinality post-filter); none are shipped yet, and
the Layer 2 ranker is what currently keeps these false positives
near the bottom of the output.
The Q-14 recall audit closed pr-miner at 2/0/1.00 against the
Semgrep open-never-closed rule on the audit corpus — the
narrow-but-clean recall signal complements the wild-corpus FP
picture.
Configuration
[detectors.pr-miner]
enabled = false
Per-language disable:
[languages.python]
suppress = ["pr-miner"]
In-source: #[cntrdct::allow(pr-miner)] (Rust) or
# cntrdct: allow(pr-miner) (Python).
Related
- arg-swap — different family of same-file API- contract violations.
- Statistical priors (P4) — pr-miner is the
only shipped detector at v0.4.3 carrying
prior_method = "wilson"(n = 38, above the Q-11SMALL_SAMPLE_THRESHOLD = 30switch). - SOTA baselines — PyBugLab pairs naturally with the arg-swap-style violations pr-miner surfaces on Python.