Repository navigation
Description
Track: false-negative
Summary
polyscan counts no decision point inside a Rust macro invocation. The branches of a tokio::select!, the body of an async_stream::try_stream! and an async move { ... } block passed to a spawn macro all score as straight-line code, so a function whose logic sits inside such a macro is reported as complexity 1 to 3 and low risk.
Rationale
Async Rust places a task's main loop inside macros. EmbarkStudios/quilkin has 70 select! sites and 5 stream!/try_stream! sites, and its event loops are some of its most complex code. They never reach the high- or medium-risk lists, so the complexity score and the hotspot list miss the code a reviewer most needs to see. The README notes that macro bodies lower clone recall, but it says nothing about complexity.
Evidence
The bodies of these functions sit inside a macro token tree. polyscan reports:
{"name":"handle_delta_discovery_responses","file_path":"crates/xds/src/config.rs","start_line":280,"end_line":369,"complexity":1,"nesting_depth":0,"risk_level":"low"}
{"name":"spawn_poll_listener_impl","file_path":"src/net/io/poll/tokio.rs","start_line":133,"end_line":298,"complexity":3,"nesting_depth":0,"risk_level":"low"}
{"name":"spawn_session","file_path":"src/net/io/poll/tokio.rs","start_line":300,"end_line":436,"complexity":3,"nesting_depth":1,"risk_level":"low"}
{"name":"MdsClient::delta_stream","file_path":"crates/xds/src/client.rs","start_line":260,"end_line":395,"complexity":9,"nesting_depth":4,"risk_level":"low"}handle_delta_discovery_responsesis 90 lines whose whole body isBox::pin(async_stream::try_stream! { for await .. { match .. } .. })(from line 288), with amatch, severalifandif letbranches and two nestedforloops.spawn_poll_listener_implwraps its body inuring_spawn!(thread_span, async move { .. })(line 153). The body holds awhile, aforover amatch, and aloop { tokio::select! { .. } }whose arms nest furthermatchandifexpressions.- With the macro wrappers removed, the two bodies score 11 (nesting 3) and 10 (nesting 4), which moves both from low to medium risk.
Repro
macro_rules! spawn_it {
($body:expr) => {
std::mem::drop($body)
};
}
pub fn plain(xs: Vec<u32>, flag: bool) -> u32 {
let mut total = 0;
for x in xs {
if x > 3 && flag {
total += x;
} else if x == 0 {
while total > 0 {
total -= 1;
}
}
}
total
}
pub fn wrapped(xs: Vec<u32>, flag: bool) {
spawn_it!(async move {
let mut total = 0;
for x in xs {
if x > 3 && flag {
total += x;
} else if x == 0 {
while total > 0 {
total -= 1;
}
}
}
total
});
}
pub async fn selected(mut a: tokio::sync::mpsc::Receiver<u32>, mut b: tokio::sync::mpsc::Receiver<u32>) {
loop {
tokio::select! {
Some(v) = a.recv() => {
if v > 10 {
break;
} else if v == 0 {
continue;
}
}
Some(w) = b.recv() => {
match w {
0 => return,
1 => continue,
_ => {}
}
}
}
}
}polyscan analyze --format json --select complexity . prints:
{"name":"plain","complexity":6,"nesting_depth":3,"risk_level":"low"}
{"name":"wrapped","complexity":1,"nesting_depth":0,"risk_level":"low"}
{"name":"selected","complexity":2,"nesting_depth":1,"risk_level":"low"}wrapped holds the same body as plain. selected counts only its loop.
Proposed change
polyscan counts the decision points and nesting of code inside a macro invocation toward the enclosing function, as it already does for closures. A macro argument that is ordinary Rust (an async move block, a stream! body) counts like the same code outside the macro. A select! with N branches counts like a match with N arms, plus the decisions inside each branch. A predicate macro such as matches! or assert! keeps counting nothing. The bundled grammar rejects for await, which try_stream! bodies use, so the count cannot depend on the macro body parsing cleanly.
Impact: med. Effort: medium to large. Confidence: high.
polyscan version 0.6.1.
Verification: survived refutation. The numbers reproduce, the README limits the macro note to clone recall, and #177 already treated the same blind spot as fixable for LCOM. Impact was lowered from high because about 50 of the repository's 2135 Rust functions are affected.
Related: #177 (macro arguments invisible to LCOM, fixed). #224 covers items inside cfg_if! and cfg_select!, where whole functions disappear.
Found via the polyscan-enhancement-audit skill in repo EmbarkStudios/quilkin@58f4f0b1b390a7c527cb0228c9267055f716d48f.
Want to work on this? Run polyscan on a project of your own first, and say in a comment which project you ran it on. See CONTRIBUTING.md, "Pull Requests".
Activity