Skip to content

Commit 719d93e

Browse files
committed
Initial commit
0 parents  commit 719d93e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+14486
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Cache Rust dependencies
2+
description: Cache Rust dependencies to speed up builds
3+
inputs:
4+
cache-name:
5+
description: The name of the cache to use
6+
required: true
7+
runs:
8+
using: composite
9+
steps:
10+
- uses: actions/cache@v4
11+
name: Cache Cargo registry
12+
id: cache-cargo-registry
13+
with:
14+
key: cargo-registry-${{ inputs.cache-name }}-${{ runner.os }}-${{ runner.arch }}
15+
path: |
16+
~/.cargo/registry/index/
17+
~/.cargo/registry/cache/
18+
~/.cargo/git/db/
19+
- uses: actions/cache@v4
20+
name: Cache Cargo target
21+
id: cache-cargo-target
22+
with:
23+
key: cargo-target-${{ inputs.cache-name }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock') }}
24+
restore-keys: |
25+
cargo-target-${{ inputs.cache-name }}-${{ runner.os }}-${{ runner.arch }}-
26+
path: |
27+
target/**
28+
- name: Set cache hit output
29+
shell: bash
30+
run: |
31+
echo "target-cache-hit=${{ steps.cache-cargo-target.outputs.cache-hit }}" >> $GITHUB_OUTPUT
32+
echo "registry-cache-hit=${{ steps.cache-cargo-registry.outputs.cache-hit }}" >> $GITHUB_OUTPUT

.github/workflows/build.yaml

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: Check, test, clippy
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
env:
16+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17+
CARGO_INCREMENTAL: 0
18+
CARGO_TERM_COLOR: always
19+
20+
jobs:
21+
check:
22+
runs-on: ${{ matrix.os }}
23+
strategy:
24+
matrix:
25+
os: [ubuntu-latest, macos-latest]
26+
steps:
27+
- uses: actions/checkout@v4
28+
- run: rustup update
29+
- uses: ./.github/actions/rust-cache
30+
with:
31+
cache-name: check
32+
- run: cargo check --all-targets
33+
test:
34+
runs-on: ${{ matrix.os }}
35+
strategy:
36+
matrix:
37+
os: [ubuntu-latest, macos-latest]
38+
steps:
39+
- uses: actions/checkout@v4
40+
- run: rustup update
41+
- uses: ./.github/actions/rust-cache
42+
with:
43+
cache-name: test
44+
- name: Install cargo-binstall
45+
uses: cargo-bins/cargo-binstall@main
46+
- name: Install cargo-nextest
47+
run: cargo binstall --no-confirm cargo-nextest
48+
- run: cargo nextest run --profile ci --cargo-profile ci
49+
clippy:
50+
runs-on: ubuntu-latest
51+
steps:
52+
- uses: actions/checkout@v4
53+
- run: rustup update
54+
- uses: ./.github/actions/rust-cache
55+
with:
56+
cache-name: clippy
57+
- run: cargo clippy --all-targets -- -D warnings
58+
fmt:
59+
runs-on: ubuntu-latest
60+
steps:
61+
- uses: actions/checkout@v4
62+
- run: cargo +nightly fmt --check
63+
deny:
64+
runs-on: ubuntu-latest
65+
steps:
66+
- uses: actions/checkout@v4
67+
- name: Install cargo-binstall
68+
uses: cargo-bins/cargo-binstall@main
69+
- name: Install cargo-deny
70+
run: cargo binstall --no-confirm cargo-deny
71+
- run: cargo deny check
72+
audit:
73+
runs-on: ubuntu-latest
74+
steps:
75+
- uses: actions/checkout@v4
76+
- name: Install cargo-binstall
77+
uses: cargo-bins/cargo-binstall@main
78+
- name: Install cargo-audit
79+
run: cargo binstall --no-confirm cargo-audit
80+
- run: cargo audit
81+
# cross-build:
82+
# runs-on: ${{ matrix.target.runner }}
83+
# strategy:
84+
# matrix:
85+
# target:
86+
# - { arch: x86_64-linux, runner: ubuntu-latest }
87+
# - { arch: aarch64-linux, runner: ubuntu-latest }
88+
# - { arch: aarch64-darwin, runner: macos-latest }
89+
# - { arch: x86_64-darwin, runner: macos-latest }
90+
# steps:
91+
# - uses: actions/checkout@v4
92+
# - name: Install nix
93+
# uses: nixbuild/nix-quick-install-action@v32
94+
# - uses: ./.github/actions/rust-cache
95+
# with:
96+
# cache-name: cross-build
97+
# - name: Determine rust target
98+
# id: rust-target
99+
# run: |
100+
# case "${{ matrix.target.arch }}" in
101+
# "x86_64-linux")
102+
# echo "rust_target=x86_64-unknown-linux-musl" >> $GITHUB_OUTPUT
103+
# ;;
104+
# "aarch64-linux")
105+
# echo "rust_target=aarch64-unknown-linux-musl" >> $GITHUB_OUTPUT
106+
# ;;
107+
# "x86_64-darwin")
108+
# echo "rust_target=x86_64-apple-darwin" >> $GITHUB_OUTPUT
109+
# ;;
110+
# "aarch64-darwin")
111+
# echo "rust_target=aarch64-apple-darwin" >> $GITHUB_OUTPUT
112+
# ;;
113+
# esac
114+
# - name: Build binary
115+
# run: |
116+
# nix develop .#crossBuildShell-${{ matrix.target.arch }} -c \
117+
# cargo build --locked --release

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target

CLAUDE.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
cargo-ferris-wheel is a Rust CLI tool for detecting and visualizing circular dependencies in Rust monorepos. It uses carnival-themed command names (inspect, spectacle, ripples, lineup, spotlight) to make dependency analysis more engaging.
8+
9+
## Common Development Commands
10+
11+
### Building and Testing
12+
```bash
13+
# Build
14+
cargo build # Debug build
15+
cargo build --release # Release build
16+
17+
# Test
18+
cargo test # Run all tests
19+
cargo nextest run --profile ci --cargo-profile ci # CI testing
20+
21+
# Code Quality
22+
cargo check --all-targets # Type checking
23+
cargo clippy --all-targets -- -D warnings # Linting
24+
cargo fmt # Format code
25+
cargo deny check # License compliance
26+
cargo audit # Security audit
27+
```
28+
29+
### Running the Tool
30+
```bash
31+
cargo run -- inspect # Check for circular dependencies
32+
cargo run -- spectacle # Generate dependency graph visualization
33+
cargo run -- ripples --files src/lib.rs # Find affected workspaces
34+
cargo run -- lineup # List all dependencies
35+
cargo run -- spotlight <crate> # Analyze specific crate
36+
```
37+
38+
## Architecture
39+
40+
The codebase follows a layered architecture:
41+
42+
1. **CLI Layer** (`cli.rs`): Command-line parsing using clap
43+
2. **Commands** (`commands/`): Command orchestration
44+
3. **Executors** (`executors/`): Business logic for each command
45+
4. **Core** (`analyzer/`, `detector/`, `graph/`):
46+
- Workspace analysis using cargo metadata
47+
- Cycle detection using Tarjan's algorithm
48+
- Graph construction with petgraph
49+
5. **Reports** (`reports/`): Output formatting (human, json, junit, github)
50+
51+
Key patterns:
52+
- Parallel processing with Rayon for performance
53+
- Comprehensive error handling with miette
54+
- Strong typing with custom error types
55+
- Multiple output formats for CI integration
56+
57+
## Key Implementation Details
58+
59+
- **Dependency Classification**: The analyzer distinguishes between direct, dev, and build dependencies
60+
- **Cycle Detection**: Uses Tarjan's strongly connected components algorithm for efficiency
61+
- **Graph Visualization**: Supports DOT, Mermaid, and ASCII output formats
62+
- **Progress Indication**: Uses indicatif for user feedback during analysis
63+
- **File Change Analysis**: Can determine affected workspaces from file changes
64+
65+
## Testing Approach
66+
67+
Tests are located alongside implementation files and in `tests/`. Run individual tests with:
68+
```bash
69+
cargo test test_name
70+
cargo test --test integration_test_name
71+
```
72+
73+
The project uses assert_cmd and predicates for CLI testing, with tempfile for test isolation.
74+
75+
## Development Best Practices
76+
77+
- Always use `cargo nextest run` instead of `cargo test` for running tests
78+
79+
## Error Handling Guidelines
80+
81+
cargo-ferris-wheel uses a carefully chosen combination of error handling crates that balance developer ergonomics with exceptional user experience:
82+
83+
- [`thiserror`](https://crates.io/crates/thiserror): For defining strongly-typed, zero-cost error enums with automatic trait implementations
84+
- [`miette`](https://crates.io/crates/miette): For rich, compiler-quality error diagnostics in user-facing applications
85+
- [`anyhow`](https://crates.io/crates/anyhow): Legacy - being phased out in favor of miette
86+
87+
**The recommendation is to use `thiserror` + `miette` for all new code.**
88+
89+
### Bottom Line Up Front
90+
91+
- **Libraries**: Define concrete error types with `thiserror`, return `Result<T, YourError>`
92+
- **Applications**: Use `miette::Result<T>` at the top level for beautiful error reporting
93+
- **Never** use `unwrap()` - use `expect()` only when certain a condition is infallible
94+
- **Always** add context when propagating errors with `.wrap_err()`
95+
- **Leverage** miette's diagnostic features for user-facing errors
96+
97+
### Error Type Definitions
98+
99+
Use `thiserror` to define error enums with zero boilerplate:
100+
101+
```rust
102+
use thiserror::Error;
103+
use miette::{Diagnostic, SourceSpan};
104+
105+
#[derive(Error, Debug, Diagnostic)]
106+
pub enum AnalyzerError {
107+
#[error("Circular dependency detected")]
108+
#[diagnostic(
109+
code(ferris_wheel::analyzer::circular_dependency),
110+
help("Remove one of the dependencies in the cycle to break it")
111+
)]
112+
CircularDependency {
113+
cycle: Vec<String>,
114+
#[source]
115+
source: detector::CycleError,
116+
},
117+
118+
#[error("Invalid workspace structure")]
119+
#[diagnostic(code(ferris_wheel::analyzer::invalid_workspace))]
120+
InvalidWorkspace {
121+
#[source_code]
122+
manifest: String,
123+
#[label("error occurs here")]
124+
span: SourceSpan,
125+
},
126+
}
127+
```
128+
129+
### Error Handling Patterns
130+
131+
#### Library Functions
132+
```rust
133+
// ✅ Good: Concrete error type with context
134+
pub fn analyze_workspace(path: &Path) -> Result<Analysis, AnalyzerError> {
135+
let metadata = cargo_metadata::MetadataCommand::new()
136+
.manifest_path(path)
137+
.exec()
138+
.map_err(|e| AnalyzerError::MetadataFailed {
139+
path: path.to_owned(),
140+
source: e,
141+
})?;
142+
143+
// ... analysis logic
144+
}
145+
```
146+
147+
#### Application Entry Points
148+
```rust
149+
use miette::{Result, IntoDiagnostic, WrapErr};
150+
151+
fn main() -> Result<()> {
152+
miette::set_panic_hook();
153+
154+
let args = Args::parse()
155+
.into_diagnostic()
156+
.wrap_err("Failed to parse command line arguments")?;
157+
158+
match args.command {
159+
Command::Inspect => inspect_command()
160+
.wrap_err("Inspection failed")?,
161+
// ... other commands
162+
}
163+
164+
Ok(())
165+
}
166+
```
167+
168+
#### Adding Context Through the Stack
169+
```rust
170+
fn process_dependencies(workspace: &Workspace) -> miette::Result<DependencyGraph> {
171+
let packages = analyze_packages(&workspace.packages)
172+
.into_diagnostic()
173+
.wrap_err_with(|| format!("Failed to analyze {} packages", workspace.packages.len()))?;
174+
175+
let graph = build_dependency_graph(packages)
176+
.map_err(|e| GraphError::BuildFailed {
177+
workspace_name: workspace.name.clone(),
178+
source: Box::new(e),
179+
})
180+
.into_diagnostic()?;
181+
182+
Ok(graph)
183+
}
184+
```
185+
186+
### Testing Error Handling
187+
188+
Never convert errors to strings for testing! Always match on the actual error type:
189+
190+
```rust
191+
#[test]
192+
fn test_circular_dependency_detection() {
193+
let result = analyze_workspace("test-fixtures/circular");
194+
195+
// ✅ GOOD: Use matches! macro
196+
assert!(matches!(
197+
result,
198+
Err(AnalyzerError::CircularDependency { cycle, .. }) if cycle.len() > 2
199+
));
200+
201+
// ❌ BAD: Don't convert to string!
202+
// assert!(result.unwrap_err().to_string().contains("circular"));
203+
}
204+
```
205+
206+
### Common Pitfalls to Avoid
207+
208+
1. **Over-wrapping Errors**: Only add meaningful context
209+
```rust
210+
// ❌ DON'T: Add redundant context
211+
let content = fs::read_to_string(path)
212+
.into_diagnostic()
213+
.wrap_err("Failed to read file") // Already in IO error
214+
.wrap_err("File operation failed")?; // Too generic
215+
216+
// ✅ DO: Add meaningful context only
217+
let content = fs::read_to_string(path)
218+
.into_diagnostic()
219+
.wrap_err_with(|| format!("Failed to load Cargo.toml from '{}'", path.display()))?;
220+
```
221+
222+
2. **Using unwrap() in Production**: Always handle errors properly
223+
```rust
224+
// ❌ DON'T: Use unwrap
225+
let metadata = get_metadata().unwrap();
226+
227+
// ✅ DO: Handle errors
228+
let metadata = get_metadata()
229+
.wrap_err("Failed to load workspace metadata")?;
230+
```
231+
232+
3. **Losing Error Information**: Preserve error structure
233+
```rust
234+
// ❌ DON'T: Convert to strings
235+
fn process() -> Result<(), String> {
236+
operation().map_err(|e| e.to_string())?;
237+
}
238+
239+
// ✅ DO: Preserve structure
240+
fn process() -> miette::Result<()> {
241+
operation()
242+
.into_diagnostic()
243+
.wrap_err("Operation failed")?;
244+
}
245+
```

0 commit comments

Comments
 (0)