Skip to content

[ENH][auto] rust/rust-macro-body-control-flow-uncounted: count decision points inside macro bodies such as tokio::select! #223

Open

Description

@DaisukeYoda

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_responses is 90 lines whose whole body is Box::pin(async_stream::try_stream! { for await .. { match .. } .. }) (from line 288), with a match, several if and if let branches and two nested for loops.
  • spawn_poll_listener_impl wraps its body in uring_spawn!(thread_span, async move { .. }) (line 153). The body holds a while, a for over a match, and a loop { tokio::select! { .. } } whose arms nest further match and if expressions.
  • 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

  1. added
    enhancementNew feature or request
    auto-filedFiled automatically by the polyscan-fp-audit skill (review before acting)
    on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    auto-filedFiled automatically by the polyscan-fp-audit skill (review before acting)enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions