Skip to content

Refactor cycle detection traversal#161

Merged
leynos merged 6 commits intomainfrom
codex/refactor-find_cycle-to-reduce-nesting-complexity
Sep 15, 2025
Merged

Refactor cycle detection traversal#161
leynos merged 6 commits intomainfrom
codex/refactor-find_cycle-to-reduce-nesting-complexity

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Sep 14, 2025

Summary

  • encapsulate graph traversal in CycleDetector for clearer state management
  • document cycle detection helper struct
  • add regression test for cycle canonicalisation

closes #74

Testing

  • make fmt
  • make check-fmt
  • make lint
  • make test
  • make markdownlint
  • make nixie

https://chatgpt.com/codex/tasks/task_e_68c615de9a808322b7eca4a379fe4f9b

Summary by Sourcery

Refactor cycle detection into a dedicated CycleDetector struct, improve canonicalize_cycle safety, expand tests, and update documentation.

Bug Fixes:

  • Guard rotate_left in canonicalize_cycle to prevent out-of-bounds slices

Enhancements:

  • Encapsulate graph traversal state and logic in a CycleDetector helper struct
  • Refactor find_cycle, visit_node, and visit_dependencies into methods on CycleDetector
  • Change should_visit_node to borrow Path references instead of cloning PathBuf

Documentation:

  • Document CycleDetector usage and self-edge handling in netsuke-design.md

Tests:

  • Add regression tests for canonicalize_cycle to verify correct rotation and reverse-direction handling

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Sep 14, 2025

Reviewer's Guide

Encapsulates cycle detection traversal into a dedicated CycleDetector struct to improve state management and readability, refactors path handling to use borrowed &Path references, updates find_cycle to leverage the new API, documents the helper in design docs, and adds regression tests for cycle canonicalization.

Class diagram for CycleDetector and related cycle detection changes

classDiagram
    class CycleDetector {
        - targets: &HashMap<PathBuf, BuildEdge>
        - stack: Vec<&Path>
        - states: HashMap<&Path, VisitState>
        + new(targets: &HashMap<PathBuf, BuildEdge>)
        + is_visited(node: &Path) bool
        + visit_node(node: &Path) Option<Vec<PathBuf>>
        + visit_dependencies(deps: &[PathBuf]) Option<Vec<PathBuf>>
    }
    class VisitState {
        <<enum>>
        Visiting
        Visited
    }
    CycleDetector --> VisitState
    CycleDetector --> BuildEdge
    BuildEdge --> PathBuf
    CycleDetector --> Path
    CycleDetector --> PathBuf
    CycleDetector --> HashMap
    CycleDetector --> Vec
    CycleDetector : -targets
    CycleDetector : -stack
    CycleDetector : -states
    CycleDetector : +new()
    CycleDetector : +is_visited()
    CycleDetector : +visit_node()
    CycleDetector : +visit_dependencies()
Loading

Class diagram for updated find_cycle function

classDiagram
    class find_cycle {
        +find_cycle(targets: &HashMap<PathBuf, BuildEdge>) Option<Vec<PathBuf>>
    }
    find_cycle --> CycleDetector
    find_cycle --> BuildEdge
    find_cycle --> PathBuf
    find_cycle --> HashMap
    find_cycle : +find_cycle()
Loading

Class diagram for canonicalize_cycle regression tests

classDiagram
    class canonicalize_cycle {
        +canonicalize_cycle(cycle: Vec<PathBuf>) Vec<PathBuf>
    }
    canonicalize_cycle --> PathBuf
    canonicalize_cycle --> Vec
    canonicalize_cycle : +canonicalize_cycle()
    class tests {
        +canonicalize_cycle_rotates_smallest_node()
        +canonicalize_cycle_handles_reverse_direction()
    }
    tests --> canonicalize_cycle
Loading

File-Level Changes

Change Details Files
Encapsulate traversal logic in CycleDetector struct
  • Introduce CycleDetector with stack and states fields
  • Implement new(), is_visited(), visit_node(), and visit_dependencies() methods
  • Refactor find_cycle to instantiate and drive the CycleDetector
src/ir.rs
Refactor state map and node handling to use &Path
  • Change should_visit_node signature to borrow &Path and return &Path on error
  • Update states map key type to &Path and remove unnecessary PathBuf cloning
  • Adjust insertion and matching logic to work with borrowed references
src/ir.rs
Document the CycleDetector helper in design documentation
  • Add description of CycleDetector’s responsibility and borrowing semantics
  • Mention self-edge rejection and deterministic cycle rotation
docs/netsuke-design.md
Add regression tests for cycle canonicalization
  • Test that canonicalize_cycle rotates to the lexicographically smallest node
  • Test correct handling of reverse-direction cycles
src/ir.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#74 Refactor the find_cycle function to reduce nested closure complexity and simplify traversal logic.
#74 Encapsulate traversal state and logic into a separate type or smaller, focused functions.
#74 Update documentation to describe the new traversal approach and helper struct.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 14, 2025

Warning

Rate limit exceeded

@leynos has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d3b6ccd and 3948b2d.

📒 Files selected for processing (2)
  • docs/netsuke-design.md (1 hunks)
  • src/ir.rs (4 hunks)

Summary by CodeRabbit

  • New Features

    • None.
  • Documentation

    • Clarified graph validation design and improved formatting and typography in the design documentation.
  • Refactor

    • Reworked internal cycle detection to be more modular and efficient while preserving behaviour and deterministic error reporting. No API changes.
  • Tests

    • Added tests ensuring deterministic cycle reporting and maintained correctness of cycle detection.

Walkthrough

Refactor cycle detection in src/ir.rs by introducing a CycleDetector struct using borrowed Path references, updating traversal and state management, and integrating with find_cycle. Adjust documentation in docs/netsuke-design.md to describe the new helper and apply formatting and typographic tweaks. Add/adjust tests for cycle canonicalisation.

Changes

Cohort / File(s) Summary
Documentation: design updates and formatting
docs/netsuke-design.md
Describe CycleDetector in Section 5.3 for DFS-based cycle validation; update numbering and minor typographic escapes; no API changes.
IR: cycle detection refactor and tests
src/ir.rs
Replace nested DFS with CycleDetector<'a> managing stack and visit states using borrowed Path; adjust imports to include Path; rework find_cycle to use detector; preserve canonicalise_cycle behaviour; update/add tests validating rotation to lexicographically smallest node.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant IR as find_cycle
  participant CD as CycleDetector
  participant CC as canonicalize_cycle

  Caller->>IR: find_cycle(targets)
  IR->>CD: new(targets)
  loop for each node
    IR->>CD: is_visited(node)?
    alt not visited
      IR->>CD: visit_node(node)
      CD->>CD: visit_dependencies(node)
      alt cycle found
        CD-->>IR: Err(cycle stack)
        IR->>CC: canonicalize(cycle)
        CC-->>IR: canonical_cycle
        IR-->>Caller: Err(canonical_cycle)
      else no cycle
        CD-->>IR: Ok
      end
    else already visited
      IR-->>IR: skip
    end
  end
  IR-->>Caller: Ok (no cycles)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

Threads of paths in ordered rows,
A watcher walks where lineage goes.
Stacks that borrow, steps that trace,
Loops unveiled in quiet grace.
Docs now tell the tale concise—
The graph breathes clean, the cuts are nice.


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

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed Accept the title as an accurate, concise summary of the primary change: it explicitly states a refactor of the cycle-detection traversal which matches the refactor in src/ir.rs and the documented CycleDetector helper. Keep the phrasing short and descriptive; it avoids file lists, emojis, and vague terms. No change required.
Linked Issues Check ✅ Passed Confirm that the changes satisfy issue #74: the traversal logic was moved into a new CycleDetector type, which reduces nested closures and simplifies find_cycle, and the PR includes documentation and a regression test for canonicalisation. Verify that the implementation preserves the public API and focuses on internal refactoring and test coverage. Approve as meeting the coding objectives from the linked issue.
Out of Scope Changes Check ✅ Passed Mark the PR as free of out-of-scope changes: edits are confined to the cycle-detection refactor in src/ir.rs, supporting unit tests, and documentation formatting; no unrelated functional code changes or public API modifications were introduced. Treat the documentation formatting tweaks as acceptable housekeeping tied to the new helper. Approve the scope as appropriate for the linked issue.
Description Check ✅ Passed Confirm that the PR description aligns with the changeset: it documents the new CycleDetector, the documentation updates to netsuke-design.md, and the added regression tests for canonicalise_cycle. Verify these items match the raw_summary and pr_objectives for src/ir.rs and the documentation change so the description is directly related to the diff. Mark the description as sufficiently informative for this lenient check.

codescene-delta-analysis[bot]

This comment was marked as outdated.

sourcery-ai[bot]

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 14, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 14, 2025

✅ 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.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 14, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 14, 2025

✅ 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
Contributor

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7348128 and d3b6ccd.

📒 Files selected for processing (2)
  • docs/netsuke-design.md (8 hunks)
  • src/ir.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/netsuke-design.md
**/*.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.

  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such

    • For testing of functionality depending upon environment variables, dependency injection and the mockable crate are the preferred option.
    • If mockable cannot be used, env mutations in tests ...

Files:

  • src/ir.rs
🔇 Additional comments (9)
docs/netsuke-design.md (5)

1084-1087: Good addition: document the traversal helper explicitly.

The explanation of CycleDetector clarifies ownership and responsibilities in the DFS stage and aligns with the refactor in src/ir.rs.


1188-1191: CLI–Ninja bridge details read cleanly and follow en‑GB spelling.

The pass‑through of flags, directory canonicalisation, and I/O capture are described precisely and respect the style guide.

Also applies to: 1193-1197, 1199-1203


1286-1289: Error‑reporting bullets improved.

The tightened wording of “Where” and “Why” keeps the guidance actionable and matches Section 7’s goals.

Also applies to: 1290-1293


1537-1538: Roadmap tasks are now unambiguous.

The phased items are clear and consistent with the current implementation order.

Also applies to: 1541-1542, 1544-1544, 1546-1546


1659-1659: Footnote metadata and style are compliant.

Date format and citation style match en‑GB and the repo’s Markdown rules.

src/ir.rs (4)

28-28: Path import is correct and required.

Importing Path alongside PathBuf enables borrowed‑key traversal without cloning.


495-501: Encapsulate traversal state (CQRS and SRP).

The CycleDetector struct reduces function complexity in line with the PR’s objective.


562-575: Top‑level driver is clean and deterministic.

Iterating nodes once and using is_visited() avoids redundant work. Canonicalisation downstream ensures consistent diagnostics.


515-546: Avoid MSRV‑brittle let‑chains and improve panic message

Replace the chained if let … && let … with nested if let to avoid raising the MSRV; update the expect("slice") to a descriptive message. Verify the project's MSRV (rust-version in Cargo.toml or rust-toolchain) before keeping let-chains.

Apply:

-        if let Some(edge) = self.targets.get(node)
-            && let Some(cycle) = self.visit_dependencies(&edge.inputs)
-        {
-            return Some(cycle);
-        }
+        if let Some(edge) = self.targets.get(node) {
+            if let Some(cycle) = self.visit_dependencies(&edge.inputs) {
+                return Some(cycle);
+            }
+        }
-                        .get(idx..)
-                        .expect("slice")
+                        .get(idx..) // idx is from position(), always <= len
+                        .expect("cycle slice must exist for previously seen node")

Verify by running:

#!/bin/bash
rg -n --type=rust -C2 $'if\\s+let\\b[^{\\n]*&&\\s*let\\b' src || true
rg -n '^rust-version\\s*=\\s*".*"' Cargo.toml || true
fd -HI 'rust-toolchain*' -t f -x sed -n '1,200p' {} || true

Comment thread src/ir.rs
Comment thread src/ir.rs
Comment thread src/ir.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 14, 2025

@coderabbitai Have the following now been resolved?

  • Avoid peeking into detector.states directly in find_cycle—expose a dedicated is_visited or should_skip method on CycleDetector to keep its internal state fully encapsulated.
  • Consider using &Path (or lightweight identifiers) instead of PathBuf for keys and stack entries to reduce cloning overhead when traversing large graphs.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 14, 2025

Tip

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

🧩 Analysis chain

Based on my analysis of the current code, I can confirm that both issues have been resolved:

@leynos:

Encapsulation Issue Resolved: The code no longer directly accesses detector.states in find_cycle. Instead, it properly uses the dedicated is_visited(&self, node: &Path) method on CycleDetector to check visitation status, maintaining proper encapsulation of the internal state.

Performance Issue Resolved: The implementation now uses &Path references throughout instead of PathBuf clones:

  • The CycleDetector struct uses HashMap<&'a Path, VisitState> for states
  • The stack uses Vec<&'a Path> instead of Vec<PathBuf>
  • Internal methods work with borrowed &Path references
  • Only converts to PathBuf when absolutely necessary (e.g., when building the final cycle result)

The refactoring successfully addresses both concerns while maintaining the same public API and functionality. The CycleDetector provides clean encapsulation with proper methods like is_visited(), visit_node(), and visit_dependencies(), and the performance is improved by avoiding unnecessary PathBuf allocations during traversal.


🏁 Script executed:

#!/bin/bash
# Examine the current implementation in src/ir.rs to check the specific concerns
cat src/ir.rs

Length of output: 20341


@leynos: Yes, both issues have been fully resolved! Here's the evidence:

Encapsulation Issue Resolved:

  • The find_cycle function now uses the dedicated is_visited(&self, node: &Path) -> bool method on line 485:
    if detector.is_visited(node) {
        continue;
    }
  • No direct access to detector.states anywhere in the code - proper encapsulation is maintained

Performance Issue Resolved:

  • CycleDetector now uses HashMap<&'a Path, VisitState> for internal state (not HashMap<PathBuf, VisitState>)
  • Stack uses Vec<&'a Path> instead of Vec<PathBuf>
  • All internal traversal methods work with borrowed &Path references
  • PathBuf allocation only occurs when building the final cycle result for return

The refactoring successfully addresses both performance and encapsulation concerns while maintaining the same public API and functionality. The CycleDetector provides clean abstraction boundaries with proper accessor methods.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 15, 2025

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@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 there - I've reviewed your changes and found some issues that need to be addressed.

  • Consider consolidating the self‐edge cycle check into your core visiting logic (e.g. in should_visit_node) instead of special-casing it in visit_dependencies, so you don’t duplicate handling.
  • The chained if let Some(edge) = … && let Some(cycle) = … in visit_node is concise but a bit cryptic—splitting it into nested if let or a match could improve readability.
  • Since any detected cycle always has length ≥2, you could simplify canonicalize_cycle by removing the conditional guard around rotate_left and directly rotating the prefix slice, making the intent clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider consolidating the self‐edge cycle check into your core visiting logic (e.g. in `should_visit_node`) instead of special-casing it in `visit_dependencies`, so you don’t duplicate handling.
- The chained `if let Some(edge) = … && let Some(cycle) = …` in `visit_node` is concise but a bit cryptic—splitting it into nested `if let` or a `match` could improve readability.
- Since any detected cycle always has length ≥2, you could simplify `canonicalize_cycle` by removing the conditional guard around `rotate_left` and directly rotating the prefix slice, making the intent clearer.

## Individual Comments

### Comment 1
<location> `src/ir.rs:596` </location>
<code_context>
         .min_by(|(_, a), (_, b)| a.cmp(b))
         .map_or(0, |(idx, _)| idx);
-    cycle.rotate_left(start);
+    if let Some(slice) = cycle.get_mut(..len) {
+        slice.rotate_left(start);
+    }
</code_context>

<issue_to_address>
**question:** Using get_mut(..len) for rotation may be less clear than rotating the whole vector.

Rotating only a slice may behave differently if len < cycle.len(). Please verify this matches the intended logic for all cases.
</issue_to_address>

### Comment 2
<location> `src/ir.rs:496` </location>
<code_context>
-    ) -> Option<Vec<PathBuf>> {
-        match should_visit_node(states, node) {
+/// Detects cycles in a dependency graph by tracking traversal state.
+struct CycleDetector<'a> {
+    targets: &'a HashMap<PathBuf, BuildEdge>,
+    stack: Vec<&'a Path>,
</code_context>

<issue_to_address>
**issue (complexity):** Consider using owned PathBufs instead of borrowed &Path references to simplify the cycle detection logic and remove lifetime annotations.

Consider dropping the `&Path`‐based state entirely and just operate on owned `PathBuf`s. This lets you remove all those lifetimes and simplify both `CycleDetector` and `find_cycle`. For example:

```rust
enum VisitState { Visiting, Visited }

struct CycleDetector<'a> {
    targets: &'a HashMap<PathBuf, BuildEdge>,
    stack: Vec<PathBuf>,
    states: HashMap<PathBuf, VisitState>,
}

impl<'a> CycleDetector<'a> {
    fn find_cycle(targets: &'a HashMap<PathBuf, BuildEdge>) -> Option<Vec<PathBuf>> {
        let mut det = CycleDetector { targets, stack: Vec::new(), states: Default::default() };
        for node in targets.keys() {
            if det.states.contains_key(node) { continue }
            if let Some(cycle) = det.visit(node.clone()) {
                return Some(cycle);
            }
        }
        None
    }

    fn visit(&mut self, node: PathBuf) -> Option<Vec<PathBuf>> {
        match self.states.get(&node) {
            Some(VisitState::Visited) => return None,
            Some(VisitState::Visiting) => {
                let idx = self.stack.iter().position(|n| n == &node).unwrap();
                let mut cycle = self.stack[idx..].to_vec();
                cycle.push(node.clone());
                return Some(canonicalize_cycle(cycle));
            }
            None => { self.states.insert(node.clone(), VisitState::Visiting); }
        }

        self.stack.push(node.clone());
        if let Some(edge) = self.targets.get(&node) {
            for dep in &edge.inputs {
                if self.targets.contains_key(dep) {
                    if let Some(cycle) = self.visit(dep.clone()) {
                        return Some(cycle);
                    }
                }
            }
        }
        self.stack.pop();
        self.states.insert(node, VisitState::Visited);
        None
    }
}

// usage:
fn find_cycle(targets: &HashMap<PathBuf, BuildEdge>) -> Option<Vec<PathBuf>> {
    CycleDetector::find_cycle(targets)
}
```

This:

- Eliminates all `&'a Path` keys and the related lifetimes.
- Collapses `visit_node`/`visit_dependencies` into one `visit`.
- Keeps the same behavior and cycle‐canonicalization.
</issue_to_address>

### Comment 3
<location> `src/ir.rs:496` </location>
<code_context>
-    ) -> Option<Vec<PathBuf>> {
-        match should_visit_node(states, node) {
+/// Detects cycles in a dependency graph by tracking traversal state.
+struct CycleDetector<'a> {
+    targets: &'a HashMap<PathBuf, BuildEdge>,
+    stack: Vec<&'a Path>,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural tests for the new CycleDetector struct to demonstrate its correct operation in cycle detection scenarios.

While unit tests for canonicalize_cycle have been added, there are no direct behavioural tests for CycleDetector itself. Add tests that exercise CycleDetector's public methods, verifying correct cycle detection and traversal state management.

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

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 4
<location> `src/ir.rs:495` </location>
<code_context>
     }
 }

-fn find_cycle(targets: &HashMap<PathBuf, BuildEdge>) -> Option<Vec<PathBuf>> {
-    fn visit(
-        targets: &HashMap<PathBuf, BuildEdge>,
</code_context>

<issue_to_address>
**issue (review_instructions):** The module does not begin with a `//!` comment as required by the instructions.

Please add a `//!` module-level comment at the top of this file to describe its purpose, as per the review instructions.

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

**Path patterns:** `**/*.rs`

**Instructions:**
Every module **must** begin with a `//!` comment.

</details>
</issue_to_address>

### Comment 5
<location> `docs/netsuke-design.md:1103` </location>
<code_context>
+
+   Traversal state is managed by a small `CycleDetector` helper struct. This
+   type holds the recursion stack and visitation map, allowing the traversal
+   functions to remain focused and easily testable. The detector borrows path
+   references from the `targets` map, so `targets` must remain unchanged during
+   detection.
</code_context>

<issue_to_address>
**issue (review_instructions):** This paragraph line exceeds the 80 column wrapping limit for paragraphs and bullets.

Please wrap this paragraph so that no line exceeds 80 columns, in accordance with the documentation style guide.

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

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

**Instructions:**
Paragraphs and bullets must be wrapped to 80 columns

</details>
</issue_to_address>

### Comment 6
<location> `docs/netsuke-design.md:1104` </location>
<code_context>
+   Traversal state is managed by a small `CycleDetector` helper struct. This
+   type holds the recursion stack and visitation map, allowing the traversal
+   functions to remain focused and easily testable. The detector borrows path
+   references from the `targets` map, so `targets` must remain unchanged during
+   detection.

</code_context>

<issue_to_address>
**issue (review_instructions):** This paragraph line exceeds the 80 column wrapping limit for paragraphs and bullets.

Please wrap this paragraph so that no line exceeds 80 columns, in accordance with the documentation style guide.

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

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

**Instructions:**
Paragraphs and bullets must be wrapped to 80 columns

</details>
</issue_to_address>

### Comment 7
<location> `docs/netsuke-design.md:1105` </location>
<code_context>
+   type holds the recursion stack and visitation map, allowing the traversal
+   functions to remain focused and easily testable. The detector borrows path
+   references from the `targets` map, so `targets` must remain unchanged during
+   detection.

 ### 5.4 Ninja File Synthesis (`ninja_gen.rs`)
</code_context>

<issue_to_address>
**issue (review_instructions):** This paragraph line exceeds the 80 column wrapping limit for paragraphs and bullets.

Please wrap this paragraph so that no line exceeds 80 columns, in accordance with the documentation style guide.

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

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

**Instructions:**
Paragraphs and bullets must be wrapped to 80 columns

</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.

Comment thread src/ir.rs
Comment thread src/ir.rs
@leynos leynos merged commit 428fa6f into main Sep 15, 2025
7 checks passed
@leynos leynos deleted the codex/refactor-find_cycle-to-reduce-nesting-complexity branch September 15, 2025 20: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.

Refactor find_cycle function to reduce nested closure complexity

1 participant