Skip to content

Enrich panic detection for interpolated panics in tests (PanicInfo)#190

Merged
leynos merged 25 commits intomainfrom
allow-unwrap-else-panic-in-tests-j92i13
Apr 10, 2026
Merged

Enrich panic detection for interpolated panics in tests (PanicInfo)#190
leynos merged 25 commits intomainfrom
allow-unwrap-else-panic-in-tests-j92i13

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Apr 7, 2026

Summary

  • Extend panic detection to recognise interpolation in panic messages inside tests
  • Introduce PanicInfo to convey whether a panic occurs and whether it uses runtime interpolation
  • Update policy logic to allow interpolated panics in tests while keeping production safeguards
  • Expand tests and UI tests to cover interpolated vs plain panics in test vs production contexts
  • Update documentation and ADR wording accordingly

Changes

Core logic

  • Panic detector now collects detailed PanicInfo: whether a closure panics and whether the panic message interpolates runtime values.
  • closure_panics(cx, body_id) now returns PanicInfo instead of a bool.
  • Detection now distinguishes between plain panics and panics that use runtime formatting arguments (e.g., fmt::Arguments::new_v1/new_v1_formatted).
  • Added mechanism to walk arguments and identify runtime format-args constructors, enabling accurate interpolation detection.

Policy

  • should_flag now accepts a PanicInfo reference instead of a boolean.
  • In test contexts, panics that interpolate runtime values are exempt (i.e., not flagged).
  • Plain panics inside tests remain flaggable, and production/panic behavior remains governed by existing rules (e.g., in_main).

API & internal wiring

  • Updated driver.rs to pass PanicInfo to should_flag instead of a boolean panic signal.
  • lib.rs documentation expanded to describe test behavior for interpolated panics.

Tests

  • Behaviour tests updated to use PanicInfo and cover interpolated vs plain panics in tests.
  • New scenarios added to policy.feature to reflect interpolated-panic-permitted-in-tests and plain-panic-in-test behavior.
  • Updated unit tests in policy.rs to validate new handling: skip interpolated panics in tests, flag plain panics, and preserve production behavior.

UI tests

  • Updated existing UI tests to reflect new naming and expectations for interpolated vs plain panics in tests.
  • Added new UI tests to demonstrate that interpolating panics outside tests are still denied (production), while interpolated panics inside tests are allowed.
  • Included new rustc flags for test edition 2021 where appropriate.

Documentation & ADR

  • docs/adr-002-dylint-expect-attribute-macro.md wording adjusted for phrasing about check cfg diagnostics.
  • Feature file policy.feature updated to reflect new scenarios and messaging.

Test plan

  • Run unit tests for panic detector and policy: ensure interpolated panics in tests are not flagged, plain panics in tests are flagged, production behavior unchanged.
  • Run UI tests: verify bad_in_test UI now differentiates between plain vs interpolating panics in tests; confirm new production-with-interpolation UI failure is emitted as expected.
  • Validate feature-mode scenarios in tests/features/policy.feature for interpolated vs plain panics in test contexts.

Notes

  • This change enables richer failure diagnostics in tests by permitting interpolated panic messages, aligning lint behavior with improved test failure reporting.
  • No API breaking changes for downstream crates; however, internal interfaces now expose PanicInfo instead of a simple boolean through the lint path.

◳ Generated by DevBoxer


ℹ️ Tag @devboxerhub to ask questions and address PR feedback

📎 Task: https://www.devboxer.com/task/ac994937-e677-4025-abc5-f36329d6f327

📝 Closes #188

Summary by Sourcery

Allow interpolating panicking unwrap_or_else fallbacks in test contexts while preserving strict behavior in production, based on richer panic detection.

New Features:

  • Detect whether panic messages use runtime interpolation when analyzing unwrap_or_else closures.
  • Treat interpolating panics inside test functions as exempt from the no_unwrap_or_else_panic lint.

Enhancements:

  • Propagate structured PanicInfo instead of a boolean through the linting pipeline for more nuanced policy decisions.
  • Improve detection of test contexts under the rustc --test harness by recovering test functions from synthesized descriptors.

Documentation:

  • Update crate and ADR documentation to describe the special handling of interpolating panics in tests and clarify cfg/check-cfg wording.

Tests:

  • Extend behavior and feature tests to cover interpolated vs plain panics across test, doctest, and production contexts.
  • Add UI tests confirming interpolated panics are allowed in tests but still denied in production code.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

Overview

This PR enriches panic detection to distinguish between plain and interpolated panics in tests, enabling selective exemption of unwrap_or_else(|| panic!(...)) in test contexts when the panic message includes runtime values. It implements the requirements from issue #188 and introduces a new PanicInfo structure to track panic characteristics throughout the linting pipeline.

Key Changes

Core Panic Detection (panic_detector.rs)

  • API Change: closure_panics() now returns PanicInfo struct instead of bool, recording three fields:

    • panics: bool — whether any panic was detected
    • has_plain_panic: bool — whether any non-interpolated panics exist
    • has_interpolated_panic: bool — whether any runtime-formatted panics exist
    • New helper method is_interpolated_only() for policy conditions
  • Detection Logic: Visitor now:

    • Continues walking expressions after finding a panic (instead of early-return), allowing detection of mixed panic types
    • Classifies unwrap/expect calls as plain panics
    • Analyzes panic! and panic_fmt! calls by inspecting first argument for core::fmt::Arguments constructors
    • Uses robust symbol matching via lang item (format_arguments) for Arguments::new_v1 and Arguments::new_v1_formatted instead of fragile string-based checks

Policy Updates (policy.rs)

  • should_flag() signature updated to accept &PanicInfo instead of bool
  • New suppression rule: interpolated-only panics in test contexts return false (not flagged)
  • Plain panics remain flaggable even in tests; production behaviour unchanged
  • Unit tests refactored from individual functions to parametrised rstest-driven policy_evaluation with explicit test cases covering safe/panicking, plain/interpolated, test/production contexts

Test Harness Integration (driver.rs)

  • NoUnwrapOrElsePanic now records is_test_harness state and precomputes a HashSet of harness-associated test HirIds
  • New summarise_context_with_harness() function extends context summary with harness-aware test detection when enabled
  • Decouples panic detection from policy evaluation by computing panic_info separately before should_flag() call

Public API Additions (src/hir.rs)

Two new public functions centralise harness test-function detection:

  • collect_harness_test_functions(cx) -> HashSet<HirId> — flat two-pass scan over crate items, matching synthesised const descriptors with fn candidates
  • collect_rstest_companion_test_functions(cx) -> HashSet<HirId> — recursive module traversal detecting rstest companion modules

These replace recursive helpers in no_expect_outside_tests/src/driver/mod.rs, reducing code duplication across lints.

Test Coverage

  • Behaviour tests: Updated DecisionWorld to use PanicInfo instead of boolean; added scenarios for interpolated panics in test contexts
  • Feature tests (policy.feature): New scenario for panicking fallback with interpolation in test context
  • UI tests:
    • bad_in_test.rs renamed function and updated to verify plain panics in tests are still flagged
    • New bad_production_with_interpolation.rs confirms interpolated panics remain flagged in production
    • New ok_in_test_with_interpolation.rs confirms interpolated panics in tests are not flagged
    • Files include --edition=2021 and --test rustc flags where applicable

Documentation

  • lib.rs: Crate-level docs extended to document exemption for test-context interpolated panics alongside existing doctest and optional main-context allowances
  • ADR-002: Definition of "configuration (cfg)" added at first use; check-cfg diagnostics explanation improved for clarity

Impact

  • Non-breaking: Only internal closure_panics() return type changes; no exported public API alterations in primary crate
  • Behaviour: .unwrap_or_else(|| panic!("interpolated: {value}")) now allowed only in test contexts; production linting unchanged
  • Test quality: Enables more diagnostic interpolated panics in test code (e.g., including runtime error values) whilst maintaining production-code strictness
  • Code reuse: Harness-detection logic centralised in Whitaker's shared src/hir.rs, reducing duplication in no_expect_outside_tests and making the helpers available for other lints

Validation

  • Unit, behaviour, feature, and UI tests pass with changes
  • Test plan items completed; no downstream breaking changes
  • Closes issue #188

Walkthrough

Record harness test HIR ids when running under --test; make closure panic detection return detailed PanicInfo (panics, has_plain_panic, has_interpolated_panic); recompute ContextSummary.is_test for expressions in test mode; pass PanicInfo into policy and suppress diagnostics for interpolated‑only panics inside tests.

Changes

Cohort / File(s) Summary
Core lint & control flow
crates/no_unwrap_or_else_panic/src/driver.rs, crates/no_unwrap_or_else_panic/src/panic_detector.rs, crates/no_unwrap_or_else_panic/src/policy.rs
Collect harness test HIR ids at crate check; change closure panic detection to return PanicInfo { panics, has_plain_panic, has_interpolated_panic }; detect fmt::Arguments::new_v1/_formatted to infer interpolation; recompute ContextSummary.is_test via HIR parent traversal when --test; pass &PanicInfo into should_flag and suppress when summary.is_test && panic_info.is_interpolated_only().
Documentation
crates/no_unwrap_or_else_panic/src/lib.rs, docs/adr-002-dylint-expect-attribute-macro.md
Extend lint docs to exempt runtime‑interpolated panic! closures in test contexts; clarify ADR wording about check-cfg unexpected_cfgs.
Tests: behaviour & BDD
crates/no_unwrap_or_else_panic/src/tests/behaviour.rs, crates/no_unwrap_or_else_panic/tests/features/policy.feature
Replace boolean closure_panics with PanicInfo in BDD world; update DecisionWorld to pass &panic_info; add steps for interpolated/plain panic variants and scenario exercising interpolated‑panic exemption in tests.
UI tests: test vs production cases
crates/no_unwrap_or_else_panic/ui/bad_in_test.rs, crates/no_unwrap_or_else_panic/ui/bad_in_test.stderr, crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rs, crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rustc-flags, crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rs, crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rustc-flags, crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.stderr
Rename UI test to assert only plain panics are flagged; add UI test allowing interpolated panic in #[test] (with --test + edition flag); add UI test denying interpolated panic in production with expected diagnostics; update stderr expectations and spans.
HIR helper extraction
crates/no_expect_outside_tests/src/driver/mod.rs, src/hir.rs
Replace local harness collection logic with whitaker::hir::collect_harness_test_functions and collect_rstest_companion_test_functions; add public HIR helpers that map harness‑synthesised descriptors to function HirIds using two‑pass crate scans.
Misc: small edits
crates/no_unwrap_or_else_panic/src/lib.rs (doc small edit)
Minor doc wording update describing exempted cases.

Sequence Diagram(s)

sequenceDiagram
    participant Driver as Driver (lint driver)
    participant HIR as HIR helper
    participant Detector as Panic Detector
    participant Policy as Policy Checker

    rect rgba(200,200,255,0.5)
    Driver->>HIR: On crate check if `--test`\ncollect_harness_test_functions(cx)
    HIR-->>Driver: Return set of harness test HirIds
    end

    rect rgba(200,255,200,0.5)
    Driver->>Detector: On expr: closure_panics(cx, body_id)
    Detector->>Detector: Traverse closure HIR\nidentify panic/unwrap/expect sites
    Detector->>Detector: Inspect panic args for\n`fmt::Arguments::new_v1/_formatted` calls
    Detector-->>Driver: Return PanicInfo{panics, has_plain_panic, has_interpolated_panic}
    end

    rect rgba(255,200,200,0.5)
    Driver->>Driver: If `--test` recompute\nContextSummary.is_test via parent traversal
    Driver->>Policy: should_flag(&policy, &summary, &panic_info, is_doctest)
    Policy->>Policy: If !panic_info.panics => false
    Policy->>Policy: If summary.is_test && panic_info.is_interpolated_only() => false
    Policy->>Policy: Apply other suppressions (doctest, allow_in_main)
    Policy-->>Driver: Return decision
    Driver->>Driver: Emit diagnostic if decision == true
    end
Loading

Poem

In tests the message gathers living trace,
Detector scans each closure's hidden face,
Driver maps harness functions with care,
Policy hushes panics that interpolate there,
Lint and tests now sing with clearer air.

🚥 Pre-merge checks | ✅ 6 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Testing ⚠️ Warning Pull request introduces significant new functionality across multiple modules but lacks unit test coverage for critical algorithms in panic_detector.rs and src/hir.rs. Add unit tests in panic_detector.rs for is_fmt_args_runtime_call(), RuntimeArgsFinder visitor, and closure_panics(); add tests in src/hir.rs for collect_harness_test_functions() and collect_rstest_companion_test_functions().
User-Facing Documentation ⚠️ Warning Pull request introduces user-facing functionality for interpolated panics in tests but fails to document this new exemption in docs/users-guide.md under the no_unwrap_or_else_panic section. Update docs/users-guide.md to document interpolated panic exemption in test contexts and revise 'How to fix' guidance accordingly.
Developer Documentation ⚠️ Warning PR introduces new public APIs and architectural changes without documenting them in the developer's guide, violating the requirement for contributor guidance. Document collect_harness_test_functions(), collect_rstest_companion_test_functions(), and PanicInfo in docs/developers-guide.md with usage examples.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main change: enriching panic detection with interpolation awareness and introducing PanicInfo.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing all major components including panic detection, policy logic, API changes, and test coverage.
Linked Issues check ✅ Passed The PR fully addresses issue #188 requirements: permits interpolated panics in tests via PanicInfo detection, reuses test-detection logic, maintains production safeguards, and distinguishes plain vs interpolated panics.
Out of Scope Changes check ✅ Passed All changes are within scope; they support the core objective of distinguishing interpolated panics in tests. ADR wording updates and documentation clarifications align with the panic-detection enhancement.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Module-Level Documentation ✅ Passed All modified Rust modules, including UI test files, carry appropriate module-level docstrings using the standard //! syntax with clear, descriptive documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #188

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch allow-unwrap-else-panic-in-tests-j92i13

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 7, 2026

Reviewer's Guide

Extends the no_unwrap_or_else_panic lint so it distinguishes between plain and interpolating panics, threads a new PanicInfo struct through the detection and policy layers, and relaxes the policy specifically for interpolating panics in test contexts while keeping production behaviour strict; also introduces harness-based test detection plus updated tests, UI tests, and docs.

Sequence diagram for lint evaluation using PanicInfo and test detection

sequenceDiagram
    participant CX as LateContext
    participant Pass as NoUnwrapOrElsePanic
    participant PD as panic_detector_closure_panics
    participant Summ as summarise_context
    participant Harness as collect_and_check_harness_tests
    participant Policy as should_flag
    participant Lint as emit_lint

    CX->>Pass: check_expr(cx, expr)
    Pass->>Pass: closure_body(expr)
    alt no_closure_body
        Pass-->>CX: return
    else has_closure
        Pass->>Summ: summarise_context(cx, expr.hir_id)
        Summ-->>Pass: ContextSummary
        Pass->>Harness: collect_harness_test_functions(cx) [once per crate]
        Harness-->>Pass: HashSet hir_HirId
        alt summary_is_not_test_and_is_test_harness
            Pass->>Harness: is_inside_harness_test_function(cx, expr, harness_test_functions)
            Harness-->>Pass: bool is_test
            Pass->>Pass: summary.is_test = is_test
        end
        Pass->>PD: closure_panics(cx, body_id)
        PD->>PD: visit_body(body)
        PD->>PD: visit_expr(expr)
        alt panic_call_detected
            PD->>PD: panic_args_use_interpolation(expr)
            PD-->>Pass: PanicInfo { panics true, uses_interpolation bool }
        else unwrap_or_expect_detected
            PD-->>Pass: PanicInfo { panics true, uses_interpolation false }
        else no_panic
            PD-->>Pass: PanicInfo { panics false, uses_interpolation false }
        end
        Pass->>Policy: should_flag(policy, summary, panic_info, is_doctest)
        Policy-->>Pass: bool flag
        alt flag_is_false
            Pass-->>CX: return
        else flag_is_true
            Pass->>Lint: emit_lint(cx, expr)
            Lint-->>Pass: ok
            Pass-->>CX: return
        end
    end
Loading

Class diagram for updated panic detection and policy types

classDiagram
    class NoUnwrapOrElsePanic {
        - LintPolicy policy
        - Localizer localizer
        - bool is_doctest
        - bool is_test_harness
        - HashSet~hir_HirId~ harness_test_functions
        + check_expr(cx, expr)
    }

    class PanicInfo {
        + bool panics
        + bool uses_interpolation
    }

    class PanicDetector {
        - LateContext cx
        - bool panics
        - bool uses_interpolation
        + visit_body(body)
        + visit_expr(expr)
    }

    class RuntimeArgsFinder {
        - bool found
        + visit_expr(expr)
    }

    class LintPolicy {
        + bool allow_in_main
        + new(allow_in_main bool) LintPolicy
        + default() LintPolicy
    }

    class ContextSummary {
        + bool is_test
        + bool in_main
    }

    class panic_detector_module {
        + closure_panics(cx, body_id) PanicInfo
        + is_unwrap_or_expect(cx, expr) bool
        + is_panic_call(cx, expr) bool
        + panic_args_use_interpolation(expr) bool
        + is_fmt_args_runtime_call(callee) bool
    }

    class policy_module {
        + should_flag(policy, summary, panic_info, is_doctest) bool
    }

    NoUnwrapOrElsePanic --> LintPolicy : uses
    NoUnwrapOrElsePanic --> ContextSummary : builds
    NoUnwrapOrElsePanic --> panic_detector_module : calls
    NoUnwrapOrElsePanic --> policy_module : calls

    panic_detector_module --> PanicDetector : constructs
    PanicDetector --> PanicInfo : populates

    panic_detector_module --> RuntimeArgsFinder : constructs
    RuntimeArgsFinder --> PanicDetector : informs_uses_interpolation

    policy_module --> LintPolicy : reads
    policy_module --> ContextSummary : reads
    policy_module --> PanicInfo : reads
Loading

Flow diagram for updated should_flag policy logic

flowchart TD
    A[Start should_flag] --> B{panic_info.panics}
    B -->|false| Z[Return false]
    B -->|true| C{is_doctest}
    C -->|true| Z
    C -->|false| D{summary.is_test}
    D -->|true| E{panic_info.uses_interpolation}
    E -->|true| Z
    E -->|false| F{summary.in_main}
    D -->|false| F
    F -->|true| G{policy.allow_in_main}
    G -->|true| Z
    G -->|false| H[Return true]
    F -->|false| H
Loading

File-Level Changes

Change Details Files
Thread PanicInfo through panic detection and lint policy to distinguish interpolating vs plain panics.
  • Introduce PanicInfo with panics and uses_interpolation fields and return it from closure_panics instead of a bool.
  • Extend PanicDetector to track whether a panic uses runtime interpolation by walking panic argument expressions for core::fmt::Arguments runtime constructors.
  • Update should_flag to receive PanicInfo, skip panics that use interpolation in test contexts, and expand its unit tests and behaviour tests accordingly.
crates/no_unwrap_or_else_panic/src/panic_detector.rs
crates/no_unwrap_or_else_panic/src/policy.rs
crates/no_unwrap_or_else_panic/src/tests/behaviour.rs
crates/no_unwrap_or_else_panic/tests/features/policy.feature
Detect test contexts more accurately in the driver and wire PanicInfo into the lint decision path.
  • Extend NoUnwrapOrElsePanic with is_test_harness and harness_test_functions state, initialised in check_crate depending on --test.
  • Implement collect_harness_test_functions / collect_in_item_group / is_test_descriptor to recover harness-marked test entry functions from HIR items and their synthesized const descriptors.
  • Add is_inside_harness_test_function to mark expressions as in-test when they sit inside a harness test function, and update the main lint path to compute a mutable ContextSummary, set is_test when appropriate, call closure_panics, and pass PanicInfo into should_flag.
crates/no_unwrap_or_else_panic/src/driver.rs
Update documentation, feature scenarios, and UI tests to reflect interpolated-panic allowances in tests and strict behaviour in production.
  • Clarify lib.rs crate-level docs to explain that interpolating panics in tests are exempt because they improve diagnostics.
  • Adjust ADR wording for dylint_expect to tidy phrasing around cfg diagnostics and macro ordering constraints.
  • Extend policy.feature scenarios and behaviour bindings to cover interpolated vs plain panics in tests, including new step for "panic message interpolates a value" and two new scenarios.
  • Revise bad_in_test UI test to use a plain panic message, and add new ok_in_test_with_interpolation and bad_production_with_interpolation UI tests (with rustc-flags stubs) to demonstrate allowed test interpolation and denied production interpolation.
crates/no_unwrap_or_else_panic/src/lib.rs
docs/adr-002-dylint-expect-attribute-macro.md
crates/no_unwrap_or_else_panic/tests/features/policy.feature
crates/no_unwrap_or_else_panic/src/tests/behaviour.rs
crates/no_unwrap_or_else_panic/ui/bad_in_test.rs
crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rs
crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rs
crates/no_unwrap_or_else_panic/ui/bad_in_test.stderr
crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rustc-flags
crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.stderr
crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rustc-flags

Assessment against linked issues

Issue Objective Addressed Explanation
#188 Modify the no_unwrap_or_else_panic lint so that `.unwrap_or_else( panic!(...))` is permitted only when used in a test context and the panic message interpolates a runtime value, while remaining denied in production code and for non-interpolating panics.
#188 Implement or reuse robust detection of test contexts (including harness-generated #[test] functions) so the lint can distinguish test code from production and doctest code when applying the above rule.
#188 Update tests and documentation to cover and describe the new behavior for interpolating vs plain panics in test vs production contexts.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos marked this pull request as ready for review April 8, 2026 00:05
Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In PanicDetector::visit_expr, uses_interpolation is overwritten on every panic call; if a closure has both interpolating and non-interpolating panics, the final value will depend on traversal order—consider OR-ing (self.uses_interpolation |= panic_args_use_interpolation(expr)) instead of assigning so any interpolating panic is preserved.
  • In is_fmt_args_runtime_call, you currently allocate a String via ident.name.as_str() and search FMT_ARGS_RUNTIME_METHODS; this can be simplified and made cheaper by matching directly on symbols (e.g. matches!(segment.ident.name, sym::new_v1 | sym::new_v1_formatted)) instead of using string-based membership.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PanicDetector::visit_expr`, `uses_interpolation` is overwritten on every panic call; if a closure has both interpolating and non-interpolating panics, the final value will depend on traversal order—consider OR-ing (`self.uses_interpolation |= panic_args_use_interpolation(expr)`) instead of assigning so any interpolating panic is preserved.
- In `is_fmt_args_runtime_call`, you currently allocate a `String` via `ident.name.as_str()` and search `FMT_ARGS_RUNTIME_METHODS`; this can be simplified and made cheaper by matching directly on symbols (e.g. `matches!(segment.ident.name, sym::new_v1 | sym::new_v1_formatted)`) instead of using string-based membership.

## Individual Comments

### Comment 1
<location path="crates/no_unwrap_or_else_panic/src/driver.rs" line_range="149" />
<code_context>
+/// and source span as each test function. This function scans for those
+/// descriptors to recover test-function identity after the harness has consumed
+/// the original `#[test]` attributes.
+fn collect_harness_test_functions<'tcx>(cx: &LateContext<'tcx>) -> HashSet<hir::HirId> {
+    let root_items: Vec<_> = cx
+        .tcx
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying harness test detection by replacing the recursive item-group scan with a single linear pass and wrapping `summarise_context` in a harness-aware helper so test logic lives in one place.

The main incidental complexity here comes from (a) the recursive/O(n²) harness collection and (b) the split responsibility for “is this a test?” across `check_expr` and `summarise_context`. You can keep all behaviour but simplify both with small, focused changes.

### 1. Replace recursive `collect_in_item_group` with a single linear scan

You don’t need to recurse modules or allocate `Vec`s per module. `hir_crate_items(()).items()` already gives you all items; you can do a single pass that:

* records all `(name, span)` for harness descriptors (`const`s), then  
* collects all `fn`s whose `(name, span)` appears in that set.

This removes recursion and the per-module O(n²) sibling search.

```rust
fn collect_harness_test_functions<'tcx>(cx: &LateContext<'tcx>) -> HashSet<hir::HirId> {
    use rustc_data_structures::fx::FxHashSet;
    use rustc_span::Span;

    let mut descriptors: FxHashSet<(Symbol, Span)> = FxHashSet::default();
    let mut candidate_fns: Vec<(hir::HirId, Symbol, Span)> = Vec::new();

    let crate_items = cx.tcx.hir_crate_items(());

    for item_id in crate_items.items() {
        let item = cx.tcx.hir_item(item_id);

        match &item.kind {
            hir::ItemKind::Fn(..) => {
                if let Some(ident) = item.kind.ident() {
                    candidate_fns.push((item.hir_id(), ident.name, item.span));
                }
            }
            hir::ItemKind::Const(..) => {
                if let Some(ident) = item.kind.ident() {
                    descriptors.insert((ident.name, item.span));
                }
            }
            _ => {}
        }
    }

    candidate_fns
        .into_iter()
        .filter_map(|(hir_id, name, span)| {
            descriptors.contains(&(name, span)).then_some(hir_id)
        })
        .collect()
}
```

Then you can drop `collect_in_item_group` and `is_test_descriptor` entirely, reducing the number of moving parts while keeping the same “match `(name, span)` of fn to synthesized const” behaviour.

### 2. Centralise harness-awareness in a helper `summarise_context` wrapper

The current flow:

```rust
let mut summary = summarise_context(cx, expr.hir_id);
if !summary.is_test && self.is_test_harness {
    summary.is_test =
        is_inside_harness_test_function(cx, expr, &self.harness_test_functions);
}
```

spreads test-detection logic between `check_expr` and the summariser. You can move this into a small helper that returns a fully-derived `ContextSummary`, so `check_expr` doesn’t need to mutate it:

```rust
fn summarise_context_with_harness<'tcx>(
    cx: &LateContext<'tcx>,
    hir_id: hir::HirId,
    is_test_harness: bool,
    harness_test_functions: &HashSet<hir::HirId>,
) -> ContextSummary {
    let mut summary = summarise_context(cx, hir_id);
    if !summary.is_test && is_test_harness {
        summary.is_test = is_inside_harness_test_function(
            cx,
            hir_id,
            harness_test_functions,
        );
    }
    summary
}
```

Change `is_inside_harness_test_function` to use `hir_id` instead of the full `Expr`:

```rust
fn is_inside_harness_test_function<'tcx>(
    cx: &LateContext<'tcx>,
    hir_id: hir::HirId,
    harness_test_functions: &HashSet<hir::HirId>,
) -> bool {
    cx.tcx
        .hir_parent_iter(hir_id)
        .any(|(_, node)| matches!(node, hir::Node::Item(item)
            if matches!(item.kind, hir::ItemKind::Fn { .. })
                && harness_test_functions.contains(&item.hir_id())))
}
```

And in `check_expr`:

```rust
let summary = summarise_context_with_harness(
    cx,
    expr.hir_id,
    self.is_test_harness,
    &self.harness_test_functions,
);

let panic_info = closure_panics(cx, body_id);
if !should_flag(&self.policy, &summary, &panic_info, self.is_doctest) {
    return;
}
```

This keeps all your functionality (including harness-based tests) but:

* removes the recursive item-group machinery and extra allocations,
* makes harness test detection a single linear pass over the crate, and
* centralises “is this a test?” logic so `ContextSummary` is fully constructed in one place rather than being patched post hoc in `check_expr`.
</issue_to_address>

### Comment 2
<location path="docs/adr-002-dylint-expect-attribute-macro.md" line_range="31" />
<code_context>
-  but toolchains can emit `unexpected_cfgs` diagnostics for unknown cfg keys/values
-  when `check-cfg` validation is enabled.
+  but toolchains can emit `unexpected_cfgs` diagnostics for unknown cfg
+  keys/values when `check-cfg` validation is enabled.

 The project needs an ergonomic, consistent, and low-friction mechanism for
</code_context>
<issue_to_address>
**issue (review_instructions):** The acronym “cfg” is used here without being defined on first use, which conflicts with the requirement to define uncommon acronyms.

The acronym “cfg” (configuration) appears here without definition. To comply with the instruction, consider defining it at its first occurrence in the document, for example: “configuration (`cfg`) keys/values”.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2ef7c00dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai coderabbitai bot added the Issue label Apr 8, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/no_unwrap_or_else_panic/src/driver.rs`:
- Around line 161-195: The function collect_in_item_group currently mixes
function filtering and module recursion; extract the module-descent block (the
for item in items { let hir::ItemKind::Mod ... collect_in_item_group(...) })
into a new helper, e.g., recurse_into_submodules(cx: &LateContext<'_>, items:
&[&hir::Item<'_>], marked: &mut HashSet<hir::HirId>), move the module_items
construction and recursive call into that helper, and replace the original
recursion block in collect_in_item_group with a single call to
recurse_into_submodules to improve clarity and separation of concerns.

In `@crates/no_unwrap_or_else_panic/tests/features/policy.feature`:
- Around line 42-46: Remove the duplicate scenario titled "Scenario: Plain panic
inside a test is still flagged" from the feature file (it duplicates the earlier
test panic scenario) — either delete this entire scenario block or merge its
Given/And/When/Then steps into the existing test panic scenario so there is only
one scenario asserting the same lint trigger for a plain panic inside a test;
update or remove any redundant example data within the duplicated scenario to
avoid repetition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ea2beb1-1c70-4e90-9f10-20aabfcdc633

📥 Commits

Reviewing files that changed from the base of the PR and between 502edb6 and c2ef7c0.

📒 Files selected for processing (14)
  • crates/no_unwrap_or_else_panic/src/driver.rs
  • crates/no_unwrap_or_else_panic/src/lib.rs
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • crates/no_unwrap_or_else_panic/src/policy.rs
  • crates/no_unwrap_or_else_panic/src/tests/behaviour.rs
  • crates/no_unwrap_or_else_panic/tests/features/policy.feature
  • crates/no_unwrap_or_else_panic/ui/bad_in_test.rs
  • crates/no_unwrap_or_else_panic/ui/bad_in_test.stderr
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rs
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rustc-flags
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.stderr
  • crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rs
  • crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rustc-flags
  • docs/adr-002-dylint-expect-attribute-macro.md

codescene-delta-analysis[bot]

This comment was marked as outdated.

@devboxerhub devboxerhub bot force-pushed the allow-unwrap-else-panic-in-tests-j92i13 branch from a714ea9 to 7094433 Compare April 8, 2026 13:52
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/no_unwrap_or_else_panic/src/panic_detector.rs (1)

95-104: ⚠️ Potential issue | 🟠 Major

Stop classifying a closure from the first panic site only.

Keep walking until every panic/unwrap site in the closure has been seen. The current early exit makes uses_interpolation depend on HIR visitation order, so a closure containing both panic!("static") and panic!("failed: {e}") can be exempted in tests even though it still has a plain panic path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs` around lines 95 - 104,
The current visit_expr in PanicDetector stops traversal after the first panic
(the early "if self.panics { return; }"), causing uses_interpolation to depend
on HIR order; remove that early return and instead, when is_panic_call(expr) is
true, set self.panics = true and update self.uses_interpolation =
self.uses_interpolation || panic_args_use_interpolation(expr) (i.e., OR in any
interpolation seen) but do not return—allow traversal to continue so every
panic/unwrap site is seen; update visit_expr in PanicDetector accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/no_unwrap_or_else_panic/src/driver.rs`:
- Around line 149-211: Duplicate logic for detecting harness-generated test
descriptors (functions collect_harness_test_functions and is_test_descriptor)
should be factored into shared infrastructure and reused instead of kept in two
places; extract these functions from
crates/no_unwrap_or_else_panic/src/driver.rs into the common helper module used
by the other lint (the module containing the existing descriptor-matching rule
in crates/no_expect_outside_tests/src/driver/mod.rs), make their signatures and
visibility usable across crates (e.g., pub(crate) or pub as appropriate),
replace the local definitions in no_unwrap_or_else_panic::driver with calls to
the shared helpers (keeping the same parameters: LateContext, HirId/Symbol/Span,
sibling Item), update imports/usages (replace direct calls to
collect_harness_test_functions and is_test_descriptor with the shared module
path), and run cargo build to fix any visibility/type mismatches.

In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Around line 149-190: The current RuntimeArgsFinder marks any nested
fmt::Arguments constructor (new_v1/new_v1_formatted) anywhere inside a panic
payload as an interpolated message; instead, restrict detection to the actual
panic message expression itself by only considering fmt::Arguments constructors
that are the message argument of a panic invocation. Update
panic_args_use_interpolation / RuntimeArgsFinder so you walk the HIR to find
panic invocations (the call representing the panic entry point) and then inspect
only that invocation's message argument for is_fmt_args_runtime_call; ensure
is_fmt_args_runtime_call is still used to match a TypeRelative path segment
(new_v1/new_v1_formatted) but only when that expr is exactly the panic message
expression, not an arbitrary nested call like Foo::new_v1().

In `@crates/no_unwrap_or_else_panic/src/policy.rs`:
- Around line 73-83: Several near-duplicate tests that exercise combinations of
panicking/plain and interpolated/plain cases should be collapsed into a single
parameterised test using rstest; replace the multiple one-case tests (the cases
built from PanicInfo via the panicking(...) helper and the SAFE const) with a
single #[rstest] table that enumerates (PanicInfo, uses_interpolation,
expected_result) rows. Update the test module to import rstest::rstest, use the
panicking(uses_interpolation: bool) -> PanicInfo constructor and SAFE constant
as inputs inside the table, and assert the same expectations per row so the
matrix (panics yes/no × interpolated yes/no) is covered compactly; ensure you
keep existing assertion logic but move it into the single parameterised test
function to remove duplicated tests across lines ~85–163.
- Around line 22-25: The doc comment incorrectly describes the heuristic as
detecting `_fmt` or `_display`; update it to state that the detector keys off
construction of runtime-formatted arguments via
fmt::Arguments::new_v1(_formatted) — i.e., we permit `.unwrap_or_else(||
panic!(...))` in tests only when the panic invocation receives a runtime
`fmt::Arguments` (created via fmt::Arguments::new_v1(_formatted)), not merely
calls to `_fmt`/`_display`; change the wording in policy.rs to mention
fmt::Arguments::new_v1(_formatted) and that the check looks for that constructed
`fmt::Arguments` at the panic site for interpolation detection.

---

Outside diff comments:
In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Around line 95-104: The current visit_expr in PanicDetector stops traversal
after the first panic (the early "if self.panics { return; }"), causing
uses_interpolation to depend on HIR order; remove that early return and instead,
when is_panic_call(expr) is true, set self.panics = true and update
self.uses_interpolation = self.uses_interpolation ||
panic_args_use_interpolation(expr) (i.e., OR in any interpolation seen) but do
not return—allow traversal to continue so every panic/unwrap site is seen;
update visit_expr in PanicDetector accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b53211f-c883-4956-93ce-c6a6302f68cb

📥 Commits

Reviewing files that changed from the base of the PR and between c2ef7c0 and 7094433.

📒 Files selected for processing (14)
  • crates/no_unwrap_or_else_panic/src/driver.rs
  • crates/no_unwrap_or_else_panic/src/lib.rs
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • crates/no_unwrap_or_else_panic/src/policy.rs
  • crates/no_unwrap_or_else_panic/src/tests/behaviour.rs
  • crates/no_unwrap_or_else_panic/tests/features/policy.feature
  • crates/no_unwrap_or_else_panic/ui/bad_in_test.rs
  • crates/no_unwrap_or_else_panic/ui/bad_in_test.stderr
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rs
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rustc-flags
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.stderr
  • crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rs
  • crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rustc-flags
  • docs/adr-002-dylint-expect-attribute-macro.md

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/no_unwrap_or_else_panic/src/panic_detector.rs (1)

141-182: ⚠️ Potential issue | 🟠 Major

Restrict interpolation detection to the panic message expression.

Walking the full panic subtree still treats any type-relative new_v1(_formatted) call as interpolation, so payload-only cases such as panic_any(Foo::new_v1()) get exempted in tests even though no formatted panic message exists. Inspect only the actual panic message operand before setting uses_interpolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs` around lines 141 - 182,
The current panic_args_use_interpolation walks the entire panic subtree
(panic_args_use_interpolation, RuntimeArgsFinder, is_fmt_args_runtime_call),
which causes type-relative runtime format calls anywhere in the payload to be
treated as interpolation; change it to only inspect the actual panic message
operand. Modify panic_args_use_interpolation to locate the panic call's message
argument (the format string / first formatting argument) and run
rustc_hir::intravisit::walk_expr(&mut finder, message_expr) instead of walking
the whole `expr`; keep RuntimeArgsFinder and is_fmt_args_runtime_call unchanged
but ensure you pass only the message Expr to the visitor so payload-only calls
like panic_any(Foo::new_v1()) are not considered interpolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/no_unwrap_or_else_panic/src/policy.rs`:
- Around line 87-103: The test's rstest matrix passes six separate parameters to
policy_evaluation which trips the arity lint; define a single struct (e.g.,
PolicyCase) that groups allow_in_main, is_test, in_main, panic_info: PanicInfo,
is_doctest, expected: bool, update each #[case::...] entry to pass a single
PolicyCase instance, and change the test function signature to accept #[case]
case: PolicyCase (then destructure case to local variables or access fields
inside policy_evaluation). Modify references to the original parameters inside
policy_evaluation to use the PolicyCase fields and keep the test table-driven
while reducing parameter arity.

---

Duplicate comments:
In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Around line 141-182: The current panic_args_use_interpolation walks the entire
panic subtree (panic_args_use_interpolation, RuntimeArgsFinder,
is_fmt_args_runtime_call), which causes type-relative runtime format calls
anywhere in the payload to be treated as interpolation; change it to only
inspect the actual panic message operand. Modify panic_args_use_interpolation to
locate the panic call's message argument (the format string / first formatting
argument) and run rustc_hir::intravisit::walk_expr(&mut finder, message_expr)
instead of walking the whole `expr`; keep RuntimeArgsFinder and
is_fmt_args_runtime_call unchanged but ensure you pass only the message Expr to
the visitor so payload-only calls like panic_any(Foo::new_v1()) are not
considered interpolation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1b02d652-7b21-4eb2-b0db-b3b6984686e8

📥 Commits

Reviewing files that changed from the base of the PR and between 7094433 and e491d81.

📒 Files selected for processing (5)
  • crates/no_expect_outside_tests/src/driver/mod.rs
  • crates/no_unwrap_or_else_panic/src/driver.rs
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • crates/no_unwrap_or_else_panic/src/policy.rs
  • src/hir.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
crates/no_unwrap_or_else_panic/src/policy.rs (1)

67-103: ⚠️ Potential issue | 🟠 Major

Remove the module-wide Clippy suppression and collapse test inputs into one case object.

Fix the root cause at Lines 97-103 and delete the blanket suppression at Line 67. Stop masking lint failures across the entire test module.

♻️ Proposed fix
 #[cfg(test)]
-#[allow(clippy::too_many_arguments)]
 mod tests {
     use super::*;
     use rstest::rstest;
+
+    #[derive(Clone, Copy, Debug)]
+    struct PolicyCase {
+        policy: LintPolicy,
+        context: ContextSummary,
+        panic_info: PanicInfo,
+        is_doctest: bool,
+        should_flag: bool,
+    }
@@
     #[rstest]
-    #[case::safe_closure(LintPolicy::new(false), summary(false, false), SAFE, false, false)]
+    #[case::safe_closure(PolicyCase {
+        policy: LintPolicy::new(false),
+        context: summary(false, false),
+        panic_info: SAFE,
+        is_doctest: false,
+        should_flag: false,
+    })]
@@
-    fn policy_evaluation(
-        #[case] policy: LintPolicy,
-        #[case] context: ContextSummary,
-        #[case] panic_info: PanicInfo,
-        #[case] is_doctest: bool,
-        #[case] expected: bool,
-    ) {
+    fn policy_evaluation(#[case] case: PolicyCase) {
         assert_eq!(
-            should_flag(&policy, &context, &panic_info, is_doctest),
-            expected
+            should_flag(
+                &case.policy,
+                &case.context,
+                &case.panic_info,
+                case.is_doctest
+            ),
+            case.should_flag
         );
     }
 }
#!/bin/bash
set -euo pipefail

# Verify no blanket suppression remains in this module.
rg -n --type=rust '#\[allow\(clippy::too_many_arguments\)\]' crates/no_unwrap_or_else_panic/src/policy.rs

# Verify policy test uses a single case parameter.
rg -nP --type=rust 'fn\s+policy_evaluation\s*\(\s*#\[case\]\s*case\s*:\s*PolicyCase\s*\)' crates/no_unwrap_or_else_panic/src/policy.rs

# Verify current Clippy arity threshold config in repo (if present).
fd -H 'clippy.toml|.clippy.toml' --exec sed -n '1,160p' {}

As per coding guidelines, "#[allow] is forbidden, suppressions must be narrowly scoped, and grouped inputs should reduce excessive function parameters in Rust tests."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/no_unwrap_or_else_panic/src/policy.rs` around lines 67 - 103, The
module-level Clippy suppression #[allow(clippy::too_many_arguments)] should be
removed and the many #[case] parameters collapsed into a single case object to
avoid excessive test-arity; introduce a small struct PolicyCase (derive Clone,
Copy, Debug) holding policy: LintPolicy, context: ContextSummary, panic_info:
PanicInfo, is_doctest: bool, should_flag: bool, update each rstest case to pass
a PolicyCase instance (use existing SAFE and summary helpers), and change the
test function policy_evaluation to take #[case] case: PolicyCase and call
should_flag(&case.policy, &case.context, &case.panic_info, case.is_doctest)
comparing to case.should_flag; remove the module-wide #[allow(...)] so no
blanket suppression remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/no_unwrap_or_else_panic/src/policy.rs`:
- Around line 89-96: The file's parameterized test attributes (the
#[case::safe_closure ...] .. #[case::flags_main_when_not_allowed ...] lines
referencing LintPolicy::new and panicking/summary helpers are not rustfmt'ed,
causing CI formatting failure; run rustfmt (cargo fmt) on
crates/no_unwrap_or_else_panic/src/policy.rs to reflow/wrap the #[case::...]
attributes, stage the reformatted file, and commit the changes so the CI
formatting check passes.

In `@src/hir.rs`:
- Around line 92-100: The current seed uses
cx.tcx.hir_crate_items(()).free_items() which gathers free items across the
whole crate and breaks the "same module" invariant; change
collect_harness_test_functions to start from the crate root module instead (use
cx.tcx.hir().krate() or the appropriate API to obtain the root module's items)
and pass that root module's item list into collect_in_item_group so recursion
proceeds module-by-module rather than comparing functions against unrelated
free_items; keep the rest of the logic (collect_in_item_group, marked HashSet)
unchanged.

---

Duplicate comments:
In `@crates/no_unwrap_or_else_panic/src/policy.rs`:
- Around line 67-103: The module-level Clippy suppression
#[allow(clippy::too_many_arguments)] should be removed and the many #[case]
parameters collapsed into a single case object to avoid excessive test-arity;
introduce a small struct PolicyCase (derive Clone, Copy, Debug) holding policy:
LintPolicy, context: ContextSummary, panic_info: PanicInfo, is_doctest: bool,
should_flag: bool, update each rstest case to pass a PolicyCase instance (use
existing SAFE and summary helpers), and change the test function
policy_evaluation to take #[case] case: PolicyCase and call
should_flag(&case.policy, &case.context, &case.panic_info, case.is_doctest)
comparing to case.should_flag; remove the module-wide #[allow(...)] so no
blanket suppression remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cfbf1bf7-7336-4482-9ce2-ae518b92549a

📥 Commits

Reviewing files that changed from the base of the PR and between e491d81 and 9635a05.

📒 Files selected for processing (2)
  • crates/no_unwrap_or_else_panic/src/policy.rs
  • src/hir.rs

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 9, 2026

@coderabbitai Have the following now been resolved?

Annul any requirements that violate the en-GB-oxendict spelling (-ize / -yse / -our) conventions (for example a request to replace "normalize" with "normalise" or "artefact" with "artifact"), or where the requirement unnecessarily increases cyclomatic complexity.

Please address the comments from this code review:

## Overall Comments
- In `PanicDetector::visit_expr`, `uses_interpolation` is overwritten on every panic call; if a closure has both interpolating and non-interpolating panics, the final value will depend on traversal order—consider OR-ing (`self.uses_interpolation |= panic_args_use_interpolation(expr)`) instead of assigning so any interpolating panic is preserved.
- In `is_fmt_args_runtime_call`, you currently allocate a `String` via `ident.name.as_str()` and search `FMT_ARGS_RUNTIME_METHODS`; this can be simplified and made cheaper by matching directly on symbols (e.g. `matches!(segment.ident.name, sym::new_v1 | sym::new_v1_formatted)`) instead of using string-based membership.

## Individual Comments

### Comment 1
<location path="crates/no_unwrap_or_else_panic/src/driver.rs" line_range="149" />
<code_context>
+/// and source span as each test function. This function scans for those
+/// descriptors to recover test-function identity after the harness has consumed
+/// the original `#[test]` attributes.
+fn collect_harness_test_functions<'tcx>(cx: &LateContext<'tcx>) -> HashSet<hir::HirId> {
+    let root_items: Vec<_> = cx
+        .tcx
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying harness test detection by replacing the recursive item-group scan with a single linear pass and wrapping `summarise_context` in a harness-aware helper so test logic lives in one place.

The main incidental complexity here comes from (a) the recursive/O(n²) harness collection and (b) the split responsibility for “is this a test?” across `check_expr` and `summarise_context`. You can keep all behaviour but simplify both with small, focused changes.

### 1. Replace recursive `collect_in_item_group` with a single linear scan

You don’t need to recurse modules or allocate `Vec`s per module. `hir_crate_items(()).items()` already gives you all items; you can do a single pass that:

* records all `(name, span)` for harness descriptors (`const`s), then  
* collects all `fn`s whose `(name, span)` appears in that set.

This removes recursion and the per-module O(n²) sibling search.

```rust
fn collect_harness_test_functions<'tcx>(cx: &LateContext<'tcx>) -> HashSet<hir::HirId> {
    use rustc_data_structures::fx::FxHashSet;
    use rustc_span::Span;

    let mut descriptors: FxHashSet<(Symbol, Span)> = FxHashSet::default();
    let mut candidate_fns: Vec<(hir::HirId, Symbol, Span)> = Vec::new();

    let crate_items = cx.tcx.hir_crate_items(());

    for item_id in crate_items.items() {
        let item = cx.tcx.hir_item(item_id);

        match &item.kind {
            hir::ItemKind::Fn(..) => {
                if let Some(ident) = item.kind.ident() {
                    candidate_fns.push((item.hir_id(), ident.name, item.span));
                }
            }
            hir::ItemKind::Const(..) => {
                if let Some(ident) = item.kind.ident() {
                    descriptors.insert((ident.name, item.span));
                }
            }
            _ => {}
        }
    }

    candidate_fns
        .into_iter()
        .filter_map(|(hir_id, name, span)| {
            descriptors.contains(&(name, span)).then_some(hir_id)
        })
        .collect()
}
```

Then you can drop `collect_in_item_group` and `is_test_descriptor` entirely, reducing the number of moving parts while keeping the same “match `(name, span)` of fn to synthesized const” behaviour.

### 2. Centralise harness-awareness in a helper `summarise_context` wrapper

The current flow:

```rust
let mut summary = summarise_context(cx, expr.hir_id);
if !summary.is_test && self.is_test_harness {
    summary.is_test =
        is_inside_harness_test_function(cx, expr, &self.harness_test_functions);
}
```

spreads test-detection logic between `check_expr` and the summariser. You can move this into a small helper that returns a fully-derived `ContextSummary`, so `check_expr` doesn’t need to mutate it:

```rust
fn summarise_context_with_harness<'tcx>(
    cx: &LateContext<'tcx>,
    hir_id: hir::HirId,
    is_test_harness: bool,
    harness_test_functions: &HashSet<hir::HirId>,
) -> ContextSummary {
    let mut summary = summarise_context(cx, hir_id);
    if !summary.is_test && is_test_harness {
        summary.is_test = is_inside_harness_test_function(
            cx,
            hir_id,
            harness_test_functions,
        );
    }
    summary
}
```

Change `is_inside_harness_test_function` to use `hir_id` instead of the full `Expr`:

```rust
fn is_inside_harness_test_function<'tcx>(
    cx: &LateContext<'tcx>,
    hir_id: hir::HirId,
    harness_test_functions: &HashSet<hir::HirId>,
) -> bool {
    cx.tcx
        .hir_parent_iter(hir_id)
        .any(|(_, node)| matches!(node, hir::Node::Item(item)
            if matches!(item.kind, hir::ItemKind::Fn { .. })
                && harness_test_functions.contains(&item.hir_id())))
}
```

And in `check_expr`:

```rust
let summary = summarise_context_with_harness(
    cx,
    expr.hir_id,
    self.is_test_harness,
    &self.harness_test_functions,
);

let panic_info = closure_panics(cx, body_id);
if !should_flag(&self.policy, &summary, &panic_info, self.is_doctest) {
    return;
}
```

This keeps all your functionality (including harness-based tests) but:

* removes the recursive item-group machinery and extra allocations,
* makes harness test detection a single linear pass over the crate, and
* centralises “is this a test?” logic so `ContextSummary` is fully constructed in one place rather than being patched post hoc in `check_expr`.
</issue_to_address>

### Comment 2
<location path="docs/adr-002-dylint-expect-attribute-macro.md" line_range="31" />
<code_context>
-  but toolchains can emit `unexpected_cfgs` diagnostics for unknown cfg keys/values
-  when `check-cfg` validation is enabled.
+  but toolchains can emit `unexpected_cfgs` diagnostics for unknown cfg
+  keys/values when `check-cfg` validation is enabled.

 The project needs an ergonomic, consistent, and low-friction mechanism for
</code_context>
<issue_to_address>
**issue (review_instructions):** The acronym “cfg” is used here without being defined on first use, which conflicts with the requirement to define uncommon acronyms.

The acronym “cfg” (configuration) appears here without definition. To comply with the instruction, consider defining it at its first occurrence in the document, for example: “configuration (`cfg`) keys/values”.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hir.rs`:
- Around line 117-139: The nested sibling scan in collect_in_item_group causes
O(n²) behavior; change it to a two-pass approach: first iterate items to build a
lookup (e.g., HashMap<(Symbol, Span), hir::HirId> or HashSet of keys) of
descriptor consts by detecting descriptor nodes the same way
is_harness_test_descriptor identifies them, then in the second pass iterate
function items (the existing filter matches) and perform O(1) lookups using
(ident.name, item.span) to decide whether to marked.insert(item.hir_id()); keep
recurse_into_submodules(cx, items, marked) as-is and reuse
is_harness_test_descriptor logic for key construction so behavior is unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f76a2d3-6d10-4884-8fdb-88274b48da25

📥 Commits

Reviewing files that changed from the base of the PR and between 9635a05 and d9e37af.

📒 Files selected for processing (2)
  • crates/no_unwrap_or_else_panic/src/policy.rs
  • src/hir.rs

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/hir.rs (1)

117-173: ⚠️ Potential issue | 🟠 Major

Replace recursive module scans with a crate-wide linear two-pass collector.

Line 117 through Line 173 still recurse per module and still do per-function descriptor scans. Implement the agreed flat two-pass scan over crate items and centralize matching in one pass-built index, then one pass lookup. Remove recursive descent for this path to eliminate repeated module-level scans.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hir.rs` around lines 117 - 173, collect_in_item_group and
recurse_into_submodules currently recurse per-module and perform per-function
descriptor scans; replace this with a crate-wide flat two-pass collector: first
traverse all crate items (not per-module recursion) to build a single index of
harness descriptors (e.g. map from (name, source-equal span) or name ->
Vec<(HirId, Span)>), then do a single second pass over all functions and mark
matches by looking them up in that index (update the existing marked:
HashSet<hir::HirId>); remove recurse_into_submodules and all recursive
per-module logic, and update collect_in_item_group to accept or obtain the full
crate item list (via the LateContext/tcx APIs) so matching is centralized and
O(items+descriptors) instead of repeated module scans.
crates/no_unwrap_or_else_panic/src/panic_detector.rs (1)

138-142: ⚠️ Potential issue | 🟠 Major

Restrict interpolation detection to the panic message argument.

Fix the detector scope. Line 138 traverses the whole panic call subtree, and Line 155/Line 177 treat any TypeRelative::*::new_v1(_)/new_v1_formatted(_) call as interpolation. This incorrectly exempts non-formatted payload paths (for example, panic_any(Foo::new_v1())) in tests.

Gate interpolation checks by panic entry-point kind and inspect only the actual panic message expression.

Run this read-only verification. Expect output showing full-expression traversal and symbol-only matching without message-argument scoping.

#!/bin/bash
set -euo pipefail

echo "1) Show interpolation traversal scope:"
rg -n -C3 'fn panic_args_use_interpolation|walk_expr\(&mut finder, expr\)' crates/no_unwrap_or_else_panic/src/panic_detector.rs

echo
echo "2) Show runtime constructor matcher details:"
rg -n -C3 'fn is_fmt_args_runtime_call|QPath::TypeRelative|new_v1_formatted|new_v1' crates/no_unwrap_or_else_panic/src/panic_detector.rs

echo
echo "3) Show panic entry points including panic_any:"
rg -n -C2 'panic_any|panic_fmt|panic_display|begin_panic_fmt' crates/no_unwrap_or_else_panic/src/panic_detector.rs

Also applies to: 154-161, 173-178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs` around lines 138 - 142,
The current interpolation detection walks the entire panic call subtree
(panic_args_use_interpolation + RuntimeArgsFinder) and thus treats any
TypeRelative::new_v1/_formatted call anywhere in the panic invocation as
formatted; change it to first determine the panic entry-point kind (handle
panic_any, panic_fmt, panic_display, begin_panic_fmt separately) and then only
run the interpolation finder against the actual panic message expression (pass
the message Expr to panic_args_use_interpolation or add a new helper that takes
that Expr), and ensure is_fmt_args_runtime_call / QPath::TypeRelative checks are
applied only within that scoped message-expression traversal so calls like
panic_any(Foo::new_v1()) are not misclassified.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Around line 138-142: The current interpolation detection walks the entire
panic call subtree (panic_args_use_interpolation + RuntimeArgsFinder) and thus
treats any TypeRelative::new_v1/_formatted call anywhere in the panic invocation
as formatted; change it to first determine the panic entry-point kind (handle
panic_any, panic_fmt, panic_display, begin_panic_fmt separately) and then only
run the interpolation finder against the actual panic message expression (pass
the message Expr to panic_args_use_interpolation or add a new helper that takes
that Expr), and ensure is_fmt_args_runtime_call / QPath::TypeRelative checks are
applied only within that scoped message-expression traversal so calls like
panic_any(Foo::new_v1()) are not misclassified.

In `@src/hir.rs`:
- Around line 117-173: collect_in_item_group and recurse_into_submodules
currently recurse per-module and perform per-function descriptor scans; replace
this with a crate-wide flat two-pass collector: first traverse all crate items
(not per-module recursion) to build a single index of harness descriptors (e.g.
map from (name, source-equal span) or name -> Vec<(HirId, Span)>), then do a
single second pass over all functions and mark matches by looking them up in
that index (update the existing marked: HashSet<hir::HirId>); remove
recurse_into_submodules and all recursive per-module logic, and update
collect_in_item_group to accept or obtain the full crate item list (via the
LateContext/tcx APIs) so matching is centralized and O(items+descriptors)
instead of repeated module scans.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ff9367be-efcf-449f-b5ed-296ad49002e7

📥 Commits

Reviewing files that changed from the base of the PR and between d9e37af and c702a1e.

📒 Files selected for processing (2)
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • src/hir.rs

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
crates/no_unwrap_or_else_panic/src/panic_detector.rs (1)

144-197: ⚠️ Potential issue | 🟠 Major

Stop classifying arbitrary *_::new_v1() payloads as interpolated panic messages.

Line 175 still treats any type-relative new_v1/new_v1_formatted call under the first panic argument as runtime formatting. That misclassifies cases like panic_any(MyType::new_v1()) and panic!(MyType::new_v1()) as interpolated, so test closures using those payloads are exempted even though no fmt::Arguments message was built. Match the compiler-generated format_args! constructor itself, not just the segment name.

In rustc HIR for Rust 2021/2024, how can compiler-generated `fmt::Arguments::new_v1` / `new_v1_formatted` calls from `format_args!` be distinguished from arbitrary user-defined associated functions with the same segment name inside `panic!` arguments?
src/hir.rs (1)

95-115: 🧹 Nitpick | 🔵 Trivial

Seed traversal from the root module directly.

Remove the crate-wide free_items() pre-scan at Line 96 and start from root-module item_ids. Keep module-scoped recursion, and delete is_crate_root_item afterwards.

♻️ Proposed refactor
 pub fn collect_harness_test_functions(cx: &LateContext<'_>) -> HashSet<hir::HirId> {
-    let root_items: Vec<_> = cx
-        .tcx
-        .hir_crate_items(())
-        .free_items()
-        .map(|id| cx.tcx.hir_item(id))
-        .filter(|item| is_crate_root_item(cx, item))
-        .collect();
+    let root_module = cx.tcx.hir_root_module();
+    let root_items: Vec<_> = root_module
+        .item_ids
+        .iter()
+        .map(|id| cx.tcx.hir_item(*id))
+        .collect();
     let mut marked = HashSet::new();
     collect_in_item_group(cx, &root_items, &mut marked);
     marked
 }
-
-/// Returns `true` when the item's immediate parent is the crate root rather
-/// than a nested module.
-fn is_crate_root_item(cx: &LateContext<'_>, item: &hir::Item<'_>) -> bool {
-    cx.tcx
-        .hir_parent_iter(item.hir_id())
-        .next()
-        .is_some_and(|(_, node)| matches!(node, hir::Node::Crate(_)))
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hir.rs` around lines 95 - 115, The current collect_harness_test_functions
starts by pre-scanning crate-wide free_items and filtering with
is_crate_root_item; instead, seed traversal from the root module's item_ids and
recurse module-scoped via collect_in_item_group, removing the free_items()
pre-scan and deleting is_crate_root_item. Update collect_harness_test_functions
to obtain the root module's Vec<hir::Item<'_>> via the crate root item_ids
(using cx.tcx.hir_crate_items(...) or the appropriate root module API), call
collect_in_item_group(cx, &root_items, &mut marked) the same as before, and
remove the now-unused is_crate_root_item function and any filtering logic that
depended on it so traversal originates from the root module only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Around line 39-41: PanicInfo currently collapses interpolation state into
uses_interpolation, which hides mixed panic paths; change PanicInfo to track two
booleans (e.g., has_plain_panic and has_interpolated_panic) instead of
uses_interpolation, update all code that sets/merges PanicInfo (including the
logic around the other occurrence at the 94-99 area) to set the appropriate
field when encountering interpolated vs plain panic sites, and modify
should_flag (and any test-suppression check) to only suppress flagging when
summary.is_test && has_interpolated_panic && !has_plain_panic so a closure with
any plain panic is still flagged.

In `@src/hir.rs`:
- Around line 87-90: Update the Rustdoc spelling: in the doc comment that starts
"The test harness synthesises a sibling `const` descriptor..." replace the
British-form "synthesises" with "synthesizes" so the comment conforms to the
project's en-GB-oxendict (-ize/-yse/-our) rule; locate the comment above the
function that scans for sibling `const` descriptors (the docblock describing
recovery of test-function identity) and make the single-word replacement.

---

Duplicate comments:
In `@src/hir.rs`:
- Around line 95-115: The current collect_harness_test_functions starts by
pre-scanning crate-wide free_items and filtering with is_crate_root_item;
instead, seed traversal from the root module's item_ids and recurse
module-scoped via collect_in_item_group, removing the free_items() pre-scan and
deleting is_crate_root_item. Update collect_harness_test_functions to obtain the
root module's Vec<hir::Item<'_>> via the crate root item_ids (using
cx.tcx.hir_crate_items(...) or the appropriate root module API), call
collect_in_item_group(cx, &root_items, &mut marked) the same as before, and
remove the now-unused is_crate_root_item function and any filtering logic that
depended on it so traversal originates from the root module only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 598798e8-6d4e-40c4-9449-e2876ac90346

📥 Commits

Reviewing files that changed from the base of the PR and between c702a1e and 7821c9f.

📒 Files selected for processing (2)
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • src/hir.rs

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 9, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

src/hir.rs

Comment on file

fn collect_in_item_group<'tcx>(
    cx: &LateContext<'tcx>,
    items: &[&'tcx hir::Item<'tcx>],
    marked: &mut HashSet<hir::HirId>,
) {
    // First pass: build a lookup of harness test descriptors by name.
    // Descriptors are const items synthesised by the --test harness with the
    // same name and source-equal span as their corresponding test functions.
    let mut descriptors_by_name: HashMap<rustc_span::Symbol, Vec<(hir::HirId, Span)>> =
        HashMap::new();
    for item in items.iter().copied() {
        if !matches!(item.kind, hir::ItemKind::Const(..)) {
            continue;
        }
        let Some(ident) = item.kind.ident() else {
            continue;
        };
        descriptors_by_name
            .entry(ident.name)
            .or_default()
            .push((item.hir_id(), item.span));
    }

    // Second pass: check each function against the descriptor lookup.
    // This is O(functions + descriptors × name_collisions) which in practice
    // is much faster than O(functions × all_descriptors) since most names are
    // unique.
    for item in items
        .iter()
        .copied()
        .filter(|item| matches!(item.kind, hir::ItemKind::Fn { .. }))
    {
        let Some(ident) = item.kind.ident() else {
            continue;
        };

        let Some(candidates) = descriptors_by_name.get(&ident.name) else {
            continue;
        };

        for &(desc_id, desc_span) in candidates {
            if desc_id != item.hir_id() && desc_span.source_equal(item.span) {
                marked.insert(item.hir_id());
                break;
            }
        }
    }

    recurse_into_submodules(cx, items, marked);
}

❌ New issue: Bumpy Road Ahead
collect_in_item_group has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@coderabbitai

This comment was marked as resolved.

leynos and others added 9 commits April 10, 2026 11:48
…review findings

PanicInfo now distinguishes between plain and interpolated panic sites
instead of collapsing them into a single uses_interpolation flag. This
prevents false negatives when a test closure contains both diagnostic
(interpolated) and unconditional (plain) panics - the lint now correctly
flags closures with any plain panic even if interpolated panics are also
present.

Changes to panic detection:
- PanicInfo: replaced `uses_interpolation: bool` with `has_plain_panic: bool`
  and `has_interpolated_panic: bool`
- PanicDetector: sets appropriate field based on panic_args_use_interpolation
  result instead of using |= to collapse all panics
- unwrap/expect calls are now correctly classified as plain panics
- should_flag: only suppresses in tests when has_interpolated_panic &&
  !has_plain_panic (interpolation-only), not when any interpolation exists

Additional fixes:
- Corrected British spelling "synthesises" → "synthesizes" (en-GB-oxendict)
- Removed free_items() pre-scan and is_crate_root_item filtering in
  collect_harness_test_functions; now seeds traversal directly from the
  crate root module via hir_owner_nodes(CRATE_OWNER_ID), matching the
  pattern used in recurse_into_submodules

All test cases and documentation updated to reflect the new PanicInfo
structure. Verified with full test suite: all 1244 tests pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Issue 1: Flatten test harness collection to single pass
- Replaced recursive module traversal with flat iteration over all_items.free_items()
- Keyed descriptors by (name, parent_module) to maintain sibling semantics without recursion
- Eliminated collect_in_item_group and recurse_into_submodules helper functions
- Reduced complexity from recursive tree walk to two flat passes over all items

Issue 2: Centralized context summarization with harness detection
- Introduced summarise_context_with_harness helper that returns immutable ContextSummary
- Merged attribute-based and harness-based test detection in single function
- Eliminated post-hoc mutation of summary.is_test in check_expr
- Removed now-unused is_inside_harness_test_function helper
- Driver now receives fully-derived summary instead of mutating it

Benefits:
- Simpler control flow (flat iteration vs recursive descent)
- Single source of truth for context summary derivation
- Immutable summary passed to policy layer (no mutation)
- Fewer helper functions and clearer separation of concerns

Verified with full test suite: all 1244 tests pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Applies targeted fixes identified in code review:

1. **Spelling consistency (driver.rs:115):** Changed "Summarises" to
   "Summarizes" in doc comment to adhere to project's en-GB-oxendict
   spelling convention (-ize/-yse/-our rule).

2. **Test coverage (policy.rs:92-148):** Added new rstest case
   `mixed_plain_and_interpolated_in_tests` to verify that closures
   containing both plain and interpolated panics in test contexts are
   correctly flagged. This ensures the policy logic properly handles
   the edge case where `has_plain_panic` and `has_interpolated_panic`
   are both true.

3. **BDD step clarity (behaviour.rs:53-59):** Added new additive step
   `given_also_interpolating` that sets only `has_interpolated_panic`
   without clearing `has_plain_panic`. This preserves the existing
   `given_interpolating` step's semantics (which replaces plain with
   interpolated) while providing a composable alternative for testing
   mixed-panic scenarios.

All changes verified with:
- cargo fmt --check (formatting)
- cargo clippy --all-targets --all-features (linting)
- cargo test --features dylint-driver (1245 tests pass)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Extracts nested loop bodies into two helper functions to eliminate
"Bumpy Road Ahead" code health warning from CodeScene.

**Changes:**
1. Added `const_descriptor` helper that extracts harness-descriptor
   key/value pairs from const items, replacing the first loop's nested
   if/match/if-let chain.

2. Added `harness_matched_fn` helper that checks if a function item
   matches a harness descriptor, replacing the second loop's nested
   if/match/if-let/for/if chain.

3. Refactored `collect_harness_test_functions` to delegate to these
   helpers, eliminating all nested conditionals from the top-level
   function body.

**Preserved invariants:**
- (name, parent_module) keying strategy retained to guard against
  cross-module false positives
- source_equal span comparison (not ==) preserved
- HirId inequality check (desc_id != fn_hir_id) preserved
- All 1245 tests pass without modification

The refactoring reduces cyclomatic complexity while maintaining
identical semantics and performance characteristics.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…raction

Eliminates nested conditionals in collect_harness_test_functions to
resolve CodeScene "Bumpy Road Ahead" warning (3 blocks at nesting
depth ≥ 2).

**Changes:**
1. Renamed `const_descriptor` to `descriptor_entry` and replaced
   chained `.then_some(())?` with early-return `if !matches!` guard
   for clarity.

2. Replaced `harness_matched_fn` with `fn_candidate` that returns a
   4-tuple `(fn_id, name, parent, span)` instead of performing the
   descriptor lookup internally. This moves the matching logic into
   the caller's `filter_map`, creating a flat pipeline.

3. Refactored main function to use iterator-chain style with inline
   `filter_map` closure that performs descriptor matching, eliminating
   all nested conditionals from both passes.

**Preserved invariants:**
- source_equal span comparison (not ==) preserved
- HirId inequality check (desc_id != fn_id) preserved
- (name, parent_module) keying strategy retained
- All 1245 tests pass without modification

The refactoring reduces cyclomatic complexity while maintaining
identical semantics.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Reduces conditional complexity in should_flag by moving the
interpolation-only check into a PanicInfo method, addressing CodeScene
"Complex Conditional" warning.

**Changes:**
1. Added PanicInfo::is_interpolated_only() method that encapsulates the
   logic "has_interpolated_panic && !has_plain_panic", clarifying the
   semantic intent.

2. Updated should_flag to use panic_info.is_interpolated_only() instead
   of directly checking the two boolean fields, reducing the conditional
   expression from 3 terms to 2.

**Rationale:**
The sub-expression "has_interpolated_panic && !has_plain_panic"
represents a domain property of PanicInfo ("contains only interpolated
panics") and belongs on that type. This improves readability and
encapsulation while reducing cyclomatic complexity in the policy
function.

**Preserved invariants:**
- Behaviour of should_flag identical for all inputs
- All 1245 tests pass without modification
- Only panic_detector.rs and policy.rs modified

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tion check

Fixes false positive in is_fmt_args_runtime_call where any type's new_v1
or new_v1_formatted method would be treated as format interpolation,
incorrectly suppressing the lint for code like panic_any(MyType::new_v1())
in test contexts.

**Changes:**
1. Thread LateContext through panic_args_use_interpolation and
   RuntimeArgsFinder to enable type checking.

2. Updated is_fmt_args_runtime_call to verify the receiver type is
   core::fmt::Arguments or std::fmt::Arguments via def_path_str check,
   not just checking the method name.

**Rationale:**
The previous implementation only checked method names (new_v1,
new_v1_formatted), which could match user-defined types. This caused
false negatives where panics with custom types would be incorrectly
classified as interpolated, suppressing lint warnings in tests when
they should have fired.

**Testing:**
- All 1245 tests pass
- Verified with cargo clippy and make lint

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Apply cargo fmt's preferred formatting for struct initialization,
collapsing the multi-line RuntimeArgsFinder initialization into a
single line for consistency with project formatting standards.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…on after rebase

Mergiraf auto-resolved a conflict during the rebase onto main by
removing the local `is_matching_harness_test_descriptor` function
(which our branch had moved to the shared `whitaker::hir` module),
but this also orphaned the rstest companion module detection that
main added in #195.

Restructure the local harness detection to use the shared
`collect_harness_test_functions` for standard const-descriptor
matching, then augment with a focused `collect_rstest_companion_
test_functions` pass that finds functions whose same-named sibling
module contains a harness descriptor (the pattern rstest case
expansions produce). This preserves both our branch's shared
harness scan and main's rstest companion module support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@devboxerhub devboxerhub bot force-pushed the allow-unwrap-else-panic-in-tests-j92i13 branch from 32e0923 to 23b6ff1 Compare April 10, 2026 11:56
codescene-delta-analysis[bot]

This comment was marked as outdated.

…ation

CodeScene flagged `descriptor_entry` and `fn_candidate` as duplicated
structure. Both functions guard on an ItemKind variant, extract an ident,
compute a parent HirId, and return a tuple built from (hir_id, name,
parent, span).

Extract the shared logic into `item_components`, a private helper
parameterised on a kind-predicate closure. This leaves each existing
function as a thin, purpose-named wrapper that transforms the canonical
tuple into its domain-specific return shape.

The signatures and call sites remain unchanged, preserving all existing
behaviour while eliminating the structural duplication.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/hir.rs (1)

85-107: 🧹 Nitpick | 🔵 Trivial

Extract shared item decomposition into one helper.

Remove duplicated extraction flow in descriptor_entry and fn_candidate by centralizing ident/parent/span harvesting once, then mapping per kind.

♻️ Proposed refactor
+fn item_components<'tcx>(
+    cx: &LateContext<'tcx>,
+    item: &hir::Item<'tcx>,
+) -> Option<(hir::HirId, rustc_span::Symbol, hir::HirId, Span)> {
+    let ident = item.kind.ident()?;
+    let parent: hir::HirId = cx.tcx.hir_get_parent_item(item.hir_id()).into();
+    Some((item.hir_id(), ident.name, parent, item.span))
+}
+
 fn descriptor_entry<'tcx>(
     cx: &LateContext<'tcx>,
     item: &hir::Item<'tcx>,
 ) -> Option<((rustc_span::Symbol, hir::HirId), (hir::HirId, Span))> {
     if !matches!(item.kind, hir::ItemKind::Const(..)) {
         return None;
     }
-    let ident = item.kind.ident()?;
-    let parent: hir::HirId = cx.tcx.hir_get_parent_item(item.hir_id()).into();
-    Some(((ident.name, parent), (item.hir_id(), item.span)))
+    let (id, name, parent, span) = item_components(cx, item)?;
+    Some(((name, parent), (id, span)))
 }
 
 fn fn_candidate<'tcx>(
     cx: &LateContext<'tcx>,
     item: &hir::Item<'tcx>,
 ) -> Option<(hir::HirId, rustc_span::Symbol, hir::HirId, Span)> {
     if !matches!(item.kind, hir::ItemKind::Fn { .. }) {
         return None;
     }
-    let ident = item.kind.ident()?;
-    let parent: hir::HirId = cx.tcx.hir_get_parent_item(item.hir_id()).into();
-    Some((item.hir_id(), ident.name, parent, item.span))
+    item_components(cx, item)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hir.rs` around lines 85 - 107, Extract the duplicated ident/parent/span
extraction into a single helper (e.g., fn decompose_item<'tcx>(item:
&hir::Item<'tcx>, cx: &LateContext<'tcx>) -> Option<(rustc_span::Symbol,
hir::HirId, hir::HirId, Span)>), then have descriptor_entry and fn_candidate
call that helper and map its returned tuple into their respective return shapes;
update descriptor_entry to map (name, parent, id, span) -> Some(((name, parent),
(id, span))) and fn_candidate to map -> Some((id, name, parent, span)),
preserving the same Option semantics and using the same ident() and
hir_get_parent_item calls only in the helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/no_expect_outside_tests/src/driver/mod.rs`:
- Around line 263-269: The crate-wide flat collection in root_items (built from
cx.tcx.hir_crate_items(()).free_items()) allows items from different modules to
be treated as siblings; modify collect_companion_in_group invocation so
companions are matched only within the same module scope — either (A) seed the
walk per-module (iterate modules and use module.item_ids when building the items
passed into collect_companion_in_group) or (B) when matching siblings (the logic
currently applied around the root_items sibling check), filter by the
parent/module id (compare parent scope/module of the candidate to the current
module) so only items from the same module are considered; use the same
module-scoped approach as the recursive path that uses module.item_ids and
mirror the pattern from whitaker::hir::collect_harness_test_functions.

In `@crates/no_unwrap_or_else_panic/src/driver.rs`:
- Around line 64-69: When initializing harness_test_functions, extend the
current whitaker::hir::collect_harness_test_functions(cx) result with the same
rstest companion-module augmentation used in the other driver: build the HashSet
by collecting collect_harness_test_functions(cx) and then merge/extend it with
the rstest companion-module-derived test function IDs (i.e. call the same helper
used to augment rstest-generated cases and union those results into
harness_test_functions), keeping the is_test_harness conditional and HashSet
fallback for non-test builds.

In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Around line 111-123: The visitor stops traversing into a panic! call once it
classifies it, so nested plain panics or unwrap/expect inside the panic
arguments are missed; modify the branch handling in panic_detector so that after
you detect is_panic_call(self.cx, expr) and set self.panics plus either
self.has_interpolated_panic or self.has_plain_panic (via
panic_args_use_interpolation), you still continue traversal into the expression
(i.e., call rustc_hir::intravisit::walk_expr(self, expr)); similarly ensure
is_unwrap_or_expect branch still sets flags and then walks the expression; in
short, do not short-circuit traversal—always call
rustc_hir::intravisit::walk_expr(self, expr) after classifying so nested panic
sites are recorded.

---

Duplicate comments:
In `@src/hir.rs`:
- Around line 85-107: Extract the duplicated ident/parent/span extraction into a
single helper (e.g., fn decompose_item<'tcx>(item: &hir::Item<'tcx>, cx:
&LateContext<'tcx>) -> Option<(rustc_span::Symbol, hir::HirId, hir::HirId,
Span)>), then have descriptor_entry and fn_candidate call that helper and map
its returned tuple into their respective return shapes; update descriptor_entry
to map (name, parent, id, span) -> Some(((name, parent), (id, span))) and
fn_candidate to map -> Some((id, name, parent, span)), preserving the same
Option semantics and using the same ident() and hir_get_parent_item calls only
in the helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 95b9180c-ae75-4ca3-9364-97587bd132da

📥 Commits

Reviewing files that changed from the base of the PR and between 3b73527 and 23b6ff1.

📒 Files selected for processing (16)
  • crates/no_expect_outside_tests/src/driver/mod.rs
  • crates/no_unwrap_or_else_panic/src/driver.rs
  • crates/no_unwrap_or_else_panic/src/lib.rs
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • crates/no_unwrap_or_else_panic/src/policy.rs
  • crates/no_unwrap_or_else_panic/src/tests/behaviour.rs
  • crates/no_unwrap_or_else_panic/tests/features/policy.feature
  • crates/no_unwrap_or_else_panic/ui/bad_in_test.rs
  • crates/no_unwrap_or_else_panic/ui/bad_in_test.stderr
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rs
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.rustc-flags
  • crates/no_unwrap_or_else_panic/ui/bad_production_with_interpolation.stderr
  • crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rs
  • crates/no_unwrap_or_else_panic/ui/ok_in_test_with_interpolation.rustc-flags
  • docs/adr-002-dylint-expect-attribute-macro.md
  • src/hir.rs

leynos and others added 3 commits April 10, 2026 12:46
…on after rebase

Move rstest companion test function detection from the
no_expect_outside_tests driver into the shared whitaker::hir module,
fixing a scoping bug: the original used hir_crate_items().free_items()
which flattens items across all modules, allowing false sibling matches
between items in different modules. The shared version starts from
hir_root_module().item_ids and recurses per-module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Augment the harness test function set with rstest companion module
detection, matching the pattern already used by no_expect_outside_tests.
Without this, rstest-parametrised test functions were not recognised as
tests and unwrap_or_else closures inside them would trigger false
positives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…classification

Make walk_expr unconditional in PanicDetector::visit_expr so that nested
panic sites within arguments of already-classified calls are not missed.
Previously walk_expr was in an else branch, meaning child expressions of
panic!/unwrap/expect calls were never visited.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/no_unwrap_or_else_panic/src/panic_detector.rs (1)

18-18: ⚠️ Potential issue | 🟠 Major

Exclude panic_any from interpolation detection.

panic_any accepts an arbitrary payload, not a formatted panic message. The current logic in panic_args_use_interpolation cannot distinguish between panic!("{x}") (a formatted diagnostic) and panic_any(format_args!("{x}")) (arbitrary payload), causing the latter to be misclassified as "interpolated-only" and incorrectly exempted in tests. Remove panic_any from the interpolation check or treat all panic_any calls as plain panics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs` at line 18, The
interpolation detection wrongly treats panic_any as a formatted panic; update
the logic in panic_args_use_interpolation to exclude or special-case panic_any
so it is not considered for interpolation detection: remove "panic_any" from the
&["core","panicking","panic_any"] target list (or explicitly treat calls to
panic_any as plain panics) and ensure any matching/inspection paths in
panic_args_use_interpolation and related match arms treat panic_any as
non-interpolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Line 18: The interpolation detection wrongly treats panic_any as a formatted
panic; update the logic in panic_args_use_interpolation to exclude or
special-case panic_any so it is not considered for interpolation detection:
remove "panic_any" from the &["core","panicking","panic_any"] target list (or
explicitly treat calls to panic_any as plain panics) and ensure any
matching/inspection paths in panic_args_use_interpolation and related match arms
treat panic_any as non-interpolated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e5f734e5-c40f-4dad-861e-05c065ce855b

📥 Commits

Reviewing files that changed from the base of the PR and between 23b6ff1 and 1408c2d.

📒 Files selected for processing (4)
  • crates/no_expect_outside_tests/src/driver/mod.rs
  • crates/no_unwrap_or_else_panic/src/driver.rs
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • src/hir.rs

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== panic_detector.rs: is_fmt_args_runtime_call and FMT_ARGS_RUNTIME_METHODS ==="
cat crates/no_unwrap_or_else_panic/src/panic_detector.rs

echo ""
echo "=== src/hir.rs current ==="
cat src/hir.rs

echo ""
echo "=== driver.rs (no_unwrap_or_else_panic) current ==="
cat crates/no_unwrap_or_else_panic/src/driver.rs

Length of output: 22028


@leynos: Here is the updated resolution status for both items:


Overall Comment 2 — Symbol matching in is_fmt_args_runtime_call

Resolved. The function now reads:

if !matches!(segment.ident.name, sym::new_v1 | sym::new_v1_formatted) {
    return false;
}

FMT_ARGS_RUNTIME_METHODS and all as_str() allocations are gone. A secondary string-based check against "core::fmt::Arguments" was added as a false-positive guard (verifying the receiver type), but that is a new addition beyond the scope of the original comment, not a regression of it.


Individual Comment 1 — Flat harness scan + summarise_context_with_harness

driver.rsResolvedsummarise_context_with_harness is present and check_expr receives an immutable summary with no post-hoc mutation.

src/hir.rs ⚠️ Partially resolved. collect_in_item_group and recurse_into_submodules are gone; the function is now a clean two-pass flat structure. However, both passes use .free_items() rather than .items() as the review comment specifically requested:

// current — uses free_items()
for item_id in cx.tcx.hir_crate_items(()).free_items() {

In practice, hir_crate_items().free_items() already covers all free items crate-wide (including those inside nested modules), so test functions — which are always free items, never associated items — will not be missed. The only items excluded by free_items() vs items() are associated items inside impl/trait blocks, which #[test] functions cannot be. The behaviour is therefore equivalent for this use-case.

If you wish to close the gap exactly as requested, change both occurrences of .free_items() in collect_harness_test_functions to .items():

for item_id in cx.tcx.hir_crate_items(()).items() {
    ...
}

cx.tcx
    .hir_crate_items(())
    .items()
    .filter_map(...)
    .collect()

Otherwise the current implementation is functionally correct and the free_items() choice can be left as-is.

leynos and others added 2 commits April 10, 2026 14:03
…nts lang item

Replace string-based def_path_str comparison against
"core::fmt::Arguments" / "std::fmt::Arguments" with a lang-item
check via cx.tcx.lang_items().format_arguments(). This eliminates
the fragile string match and ensures the receiver type is verified
against the canonical compiler definition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace nested conditional logic with a self-contained two-pass flat
scan in `collect_harness_test_functions`. The previous implementation
delegated to `collect_in_item_group` and `recurse_into_submodules`,
producing nested conditionals flagged by CodeScene.

The new implementation directly iterates over all free items twice:
once to collect descriptors (const items), and once to collect
candidate functions. Filtering then matches functions against
descriptors using the same invariants (parent equality, span
source_equal, and HirId inequality).

Remove `HashMap`, `descriptor_entry`, `fn_candidate`,
`is_crate_root_item`, `collect_in_item_group`, and
`recurse_into_submodules`, which are no longer needed.

In `no_unwrap_or_else_panic/src/driver.rs`, centralize context
derivation by extracting `is_inside_harness_test_function` as a
separate helper accepting `hir_id` directly. The helper is called from
`summarise_context_with_harness`, consolidating test detection logic
into a single call site rather than post-hoc mutation.

All 1293 tests pass without modification.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 10, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/no_unwrap_or_else_panic/src/panic_detector.rs (1)

18-18: ⚠️ Potential issue | 🟠 Major

Exclude panic_any from interpolation detection; treat all invocations as plain panics.

The first argument to panic_any is an arbitrary payload (M: 'static + Any + Send), not a formatted panic message. Classify all panic_any calls as plain (non-interpolated) panics regardless of the payload type. Currently, panic_any(format_args!("{value}")) is misclassified as interpolated-only and incorrectly exempted from test linting, even though it falls outside the panic!(...) exception.

Exclude panic_any from the call graph in panic_args_use_interpolation() before inspecting the first argument. Add an early return that detects panic_any calls (matching paths core::panicking::panic_any, std::panicking::panic_any, and std::panic::panic_any) and returns false to classify them as plain panics.

🛠️ Proposed fix
 fn panic_args_use_interpolation<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool {
-    // Extract the panic message argument (first argument to the panic call).
-    let ExprKind::Call(_, args) = expr.kind else {
+    let ExprKind::Call(callee, args) = expr.kind else {
         return false;
     };
+    if is_panic_any_call(cx, callee) {
+        return false;
+    }

     let Some(message_arg) = args.first() else {
         return false;
     };
@@
     rustc_hir::intravisit::walk_expr(&mut finder, message_arg);
     finder.found
 }
+
+fn is_panic_any_call(cx: &LateContext<'_>, callee: &Expr<'_>) -> bool {
+    let Some(def_id) = def_id_of_callee(cx, callee) else {
+        return false;
+    };
+
+    let path = SimplePath::from(cx.tcx.def_path_str(def_id).as_str());
+    whitaker_common::is_path_to(&path, ["core", "panicking", "panic_any"].into_iter())
+        || whitaker_common::is_path_to(&path, ["std", "panicking", "panic_any"].into_iter())
+        || whitaker_common::is_path_to(&path, ["std", "panic", "panic_any"].into_iter())
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs` at line 18, The
function panic_args_use_interpolation() currently treats calls to panic_any like
other panics and may inspect their first argument; change it to detect panic_any
early by checking the call path for "core::panicking::panic_any",
"std::panicking::panic_any", or "std::panic::panic_any" and immediately return
false so panic_any is classified as a plain (non-interpolated) panic; update the
call-graph/pre-check logic in panic_args_use_interpolation() to exclude
panic_any before any argument inspection.
♻️ Duplicate comments (1)
src/hir.rs (1)

107-135: ⚠️ Potential issue | 🟠 Major

Match harness descriptors by parent module.

Guard the flat scan with the parent HIR id as well as name and source_equal span. The current key can cross-match unrelated macro-generated items in different modules, which marks non-test functions as harness tests and suppresses diagnostics under --test.

🛠️ Proposed fix
-    let mut descriptors: Vec<(rustc_span::Symbol, Span, hir::HirId)> = Vec::new();
-    let mut candidate_fns: Vec<(hir::HirId, rustc_span::Symbol, Span)> = Vec::new();
+    let mut descriptors: Vec<(rustc_span::Symbol, Span, hir::HirId, hir::HirId)> = Vec::new();
+    let mut candidate_fns: Vec<(hir::HirId, rustc_span::Symbol, Span, hir::HirId)> = Vec::new();

     for item_id in cx.tcx.hir_crate_items(()).free_items() {
         let item = cx.tcx.hir_item(item_id);
-        let Some(ident) = item.kind.ident() else {
-            continue;
-        };
-
-        match item.kind {
-            hir::ItemKind::Const(..) => {
-                descriptors.push((ident.name, item.span, item.hir_id()));
-            }
-            hir::ItemKind::Fn { .. } => {
-                candidate_fns.push((item.hir_id(), ident.name, item.span));
-            }
-            _ => {}
-        }
+        if let Some((hir_id, name, parent, span)) =
+            item_components(cx, item, |kind| matches!(kind, hir::ItemKind::Const(..)))
+        {
+            descriptors.push((name, span, hir_id, parent));
+        } else if let Some((hir_id, name, parent, span)) =
+            item_components(cx, item, |kind| matches!(kind, hir::ItemKind::Fn { .. }))
+        {
+            candidate_fns.push((hir_id, name, span, parent));
+        }
     }

     candidate_fns
         .into_iter()
-        .filter(|(fn_id, name, span)| {
-            descriptors.iter().any(|(desc_name, desc_span, desc_id)| {
-                desc_id != fn_id && *desc_name == *name && desc_span.source_equal(*span)
+        .filter(|(fn_id, name, span, parent)| {
+            descriptors.iter().any(|(desc_name, desc_span, desc_id, desc_parent)| {
+                desc_id != fn_id
+                    && *desc_parent == *parent
+                    && *desc_name == *name
+                    && desc_span.source_equal(*span)
             })
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hir.rs` around lines 107 - 135, The matching needs to also require the
parent HIR id to avoid cross-module/macro collisions: when building descriptors
and candidate_fns, capture each item's parent HIR id (in addition to ident.name,
span and hir_id) and then change the filter to require desc_parent == fn_parent
&& desc_id != fn_id && desc_name == name && desc_span.source_equal(*span);
update the tuples in descriptors and candidate_fns to include the parent id and
use those fields (e.g., replace descriptors: Vec<(rustc_span::Symbol, Span,
hir::HirId)> and candidate_fns: Vec<(hir::HirId, rustc_span::Symbol, Span)> with
versions that include the parent id, and adjust the closure to compare
desc_parent with fn_parent).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hir.rs`:
- Around line 195-208: The helper has_companion_test_module currently treats any
sibling mod with the same name as the function as a test module regardless of
which test inside it matches; update it to require the module actually contains
a harness descriptor for the same function_name by passing function_name into
module_has_harness_descriptor (change module_has_harness_descriptor signature to
accept the function_name/target symbol and update all call sites, including the
other occurrences noted around lines 210-241) and make has_companion_test_module
call module_has_harness_descriptor(cx, sibling, function_name) so sibling mods
are accepted only when they prove a harness for that exact function name.

---

Outside diff comments:
In `@crates/no_unwrap_or_else_panic/src/panic_detector.rs`:
- Line 18: The function panic_args_use_interpolation() currently treats calls to
panic_any like other panics and may inspect their first argument; change it to
detect panic_any early by checking the call path for
"core::panicking::panic_any", "std::panicking::panic_any", or
"std::panic::panic_any" and immediately return false so panic_any is classified
as a plain (non-interpolated) panic; update the call-graph/pre-check logic in
panic_args_use_interpolation() to exclude panic_any before any argument
inspection.

---

Duplicate comments:
In `@src/hir.rs`:
- Around line 107-135: The matching needs to also require the parent HIR id to
avoid cross-module/macro collisions: when building descriptors and
candidate_fns, capture each item's parent HIR id (in addition to ident.name,
span and hir_id) and then change the filter to require desc_parent == fn_parent
&& desc_id != fn_id && desc_name == name && desc_span.source_equal(*span);
update the tuples in descriptors and candidate_fns to include the parent id and
use those fields (e.g., replace descriptors: Vec<(rustc_span::Symbol, Span,
hir::HirId)> and candidate_fns: Vec<(hir::HirId, rustc_span::Symbol, Span)> with
versions that include the parent id, and adjust the closure to compare
desc_parent with fn_parent).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 082296ff-d800-41ca-9c24-53a991d7c5c0

📥 Commits

Reviewing files that changed from the base of the PR and between 1408c2d and c50fc9f.

📒 Files selected for processing (3)
  • crates/no_unwrap_or_else_panic/src/driver.rs
  • crates/no_unwrap_or_else_panic/src/panic_detector.rs
  • src/hir.rs

@leynos leynos merged commit c5807f2 into main Apr 10, 2026
5 checks passed
@leynos leynos deleted the allow-unwrap-else-panic-in-tests-j92i13 branch April 10, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow .unwrap_or_else(|| { panic!(...) }) under limited circumstances

1 participant