From a3da010b5c2fe835c6ec2d6831d4bae08e9e8766 Mon Sep 17 00:00:00 2001 From: DeFiDude <59237470+DeFiDude@users.noreply.github.com> Date: Thu, 6 Aug 2026 18:49:06 -0600 Subject: [PATCH] ci: enforce strict Clippy across game targets --- .github/workflows/ci.yml | 20 ++++++++- clippy.toml | 2 + examples/chess_game.rs | 10 ++--- examples/tictactoe_game.rs | 28 ++++++------- src/apps/chess.rs | 86 +++++++++++++++++++------------------- src/apps/four_in_a_row.rs | 9 ++-- src/apps/tictactoe.rs | 44 +++++++++---------- src/dedup.rs | 8 ++-- src/router.rs | 41 +++++++++--------- 9 files changed, 136 insertions(+), 112 deletions(-) create mode 100644 clippy.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b087c8e..9773475 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,22 @@ jobs: - name: Check formatting run: cargo fmt --all -- --check - name: Lint all targets - run: cargo clippy --all-targets --all-features -- -D warnings + run: cargo clippy --all-targets --all-features --locked -- -D warnings - name: Run tests - run: cargo test --all-features + run: cargo test --all-features --locked + + msrv: + name: Rust 1.85 MSRV + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@1.85.0 + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + with: + cache-bin: false + save-if: ${{ github.ref == 'refs/heads/main' }} + - name: Clippy (all targets and features) + run: cargo clippy --all-targets --all-features --locked -- -D warnings diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..566161e --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +# Keep Clippy suggestions aligned with the public MSRV in Cargo.toml. +msrv = "1.85" diff --git a/examples/chess_game.rs b/examples/chess_game.rs index bec651b..ff14c90 100644 --- a/examples/chess_game.rs +++ b/examples/chess_game.rs @@ -102,11 +102,11 @@ fn main() { let result = router_b .dispatch_incoming(&move_env, player_a, player_b) .unwrap(); - if let IncomingDispatch::Applied(result) = result - && let Some(emit) = &result.emit - { - if let Some(ev_type) = emit.get("type") { - println!("B inbound event: {ev_type:?}"); + if let IncomingDispatch::Applied(result) = result { + if let Some(emit) = &result.emit { + if let Some(ev_type) = emit.get("type") { + println!("B inbound event: {ev_type:?}"); + } } } diff --git a/examples/tictactoe_game.rs b/examples/tictactoe_game.rs index 2e4ae3e..3c9c7de 100644 --- a/examples/tictactoe_game.rs +++ b/examples/tictactoe_game.rs @@ -36,10 +36,10 @@ fn main() { // Player B receives the challenge println!("\n=== Player B receives challenge ==="); let result = router.dispatch_incoming(&env, player_a, player_b).unwrap(); - if let IncomingDispatch::Applied(result) = result - && let Some(emit) = &result.emit - { - println!("Event: {:?}", emit.get("type")); + if let IncomingDispatch::Applied(result) = result { + if let Some(emit) = &result.emit { + println!("Event: {:?}", emit.get("type")); + } } // Player B accepts @@ -63,10 +63,10 @@ fn main() { let result = router .dispatch_incoming(&accept_env, player_b, player_a) .unwrap(); - if let IncomingDispatch::Applied(result) = result - && let Some(emit) = &result.emit - { - println!("Event: {:?}", emit.get("type")); + if let IncomingDispatch::Applied(result) = result { + if let Some(emit) = &result.emit { + println!("Event: {:?}", emit.get("type")); + } } // Play some moves @@ -103,12 +103,12 @@ fn main() { // Other player receives let result = router.dispatch_incoming(&move_env, player, other).unwrap(); - if let IncomingDispatch::Applied(result) = result - && let Some(emit) = &result.emit - { - if let Some(payload_val) = emit.get("payload") { - if let Some(board_val) = payload_val.get("b") { - println!("Board: {}", board_val.as_str().unwrap_or("?")); + if let IncomingDispatch::Applied(result) = result { + if let Some(emit) = &result.emit { + if let Some(payload_val) = emit.get("payload") { + if let Some(board_val) = payload_val.get("b") { + println!("Board: {}", board_val.as_str().unwrap_or("?")); + } } } } diff --git a/src/apps/chess.rs b/src/apps/chess.rs index d8a582a..803b7d7 100644 --- a/src/apps/chess.rs +++ b/src/apps/chess.rs @@ -43,10 +43,10 @@ static FORCED_COIN: Mutex> = Mutex::new(None); fn flip_responder_coin() -> bool { #[cfg(any(test, feature = "test-helpers"))] { - if let Ok(guard) = FORCED_COIN.lock() - && let Some(v) = *guard - { - return v; + if let Ok(guard) = FORCED_COIN.lock() { + if let Some(v) = *guard { + return v; + } } } use rand::RngCore; @@ -179,10 +179,10 @@ fn insufficient_material(board: &Board) -> bool { for sq in board.pieces(Piece::Bishop) & black { b_sq = Some(sq); } - if let (Some(ws), Some(bs)) = (w_sq, b_sq) - && square_is_light(ws) == square_is_light(bs) - { - return true; + if let (Some(ws), Some(bs)) = (w_sq, b_sq) { + if square_is_light(ws) == square_is_light(bs) { + return true; + } } } false @@ -1198,22 +1198,25 @@ impl ChessApp { // On claim, pre-terminate locally so UI reflects immediately. if reason == R_THREEFOLD || reason == R_FIFTY_MOVE { let moves = meta_string_list(&session.metadata, "moves"); - if let Ok(board) = replay_moves(&moves) - && claim_reason(&board, &moves) == Some(reason.as_str()) - { - let _ = - SessionStateMachine::apply_command(&mut session, CMD_DRAW_ACCEPT, false); - session - .metadata - .insert("terminal".into(), JsonValue::String("draw".into())); - session - .metadata - .insert("terminal_reason".into(), JsonValue::String(reason.clone())); - session - .metadata - .insert("turn".into(), JsonValue::String("".into())); - session.clear_draw_offer(); - completed_claim = true; + if let Ok(board) = replay_moves(&moves) { + if claim_reason(&board, &moves) == Some(reason.as_str()) { + let _ = SessionStateMachine::apply_command( + &mut session, + CMD_DRAW_ACCEPT, + false, + ); + session + .metadata + .insert("terminal".into(), JsonValue::String("draw".into())); + session + .metadata + .insert("terminal_reason".into(), JsonValue::String(reason.clone())); + session + .metadata + .insert("turn".into(), JsonValue::String("".into())); + session.clear_draw_offer(); + completed_claim = true; + } } } if !completed_claim { @@ -1260,11 +1263,11 @@ impl ChessApp { } fn handle_draw_decline_out(&self, session_id: &str, identity_id: &str) -> OutgoingResult { - if let Some(mut session) = self.get_session(session_id, identity_id) - && SessionStateMachine::apply_command(&mut session, CMD_DRAW_DECLINE, false).is_ok() - { - session.clear_draw_offer(); - self.save_session(&session); + if let Some(mut session) = self.get_session(session_id, identity_id) { + if SessionStateMachine::apply_command(&mut session, CMD_DRAW_DECLINE, false).is_ok() { + session.clear_draw_offer(); + self.save_session(&session); + } } OutgoingResult { payload: HashMap::new(), @@ -1529,16 +1532,16 @@ impl GameApp for ChessApp { sender_hash: &str, identity_id: &str, ) -> IncomingResult { - if command != CMD_ERROR - && let Err(message) = self.validate_incoming_payload( + if command != CMD_ERROR { + if let Err(message) = self.validate_incoming_payload( session_id, command, payload, identity_id, sender_hash, - ) - { - return error_result(ERR_PROTOCOL_ERROR, &message); + ) { + return error_result(ERR_PROTOCOL_ERROR, &message); + } } match command { CMD_CHALLENGE => { @@ -1820,14 +1823,13 @@ impl GameApp for ChessApp { SessionStateMachine::check_expiry(&mut session, Some(&Self::ttl_policy()), None); if session.draw_offered_by().is_none() { session.clear_draw_offer(); - } else if let Some(owner) = session.draw_offered_by() - && owner != session.identity_id - && owner != session.contact_hash - { - return Err(LrgpError::Validation { - code: ERR_PROTOCOL_ERROR.into(), - message: "draw offer owner is not a bound participant".into(), - }); + } else if let Some(owner) = session.draw_offered_by() { + if owner != session.identity_id && owner != session.contact_hash { + return Err(LrgpError::Validation { + code: ERR_PROTOCOL_ERROR.into(), + message: "draw offer owner is not a bound participant".into(), + }); + } } self.save_session(&session); Ok(()) diff --git a/src/apps/four_in_a_row.rs b/src/apps/four_in_a_row.rs index e7bee34..129303e 100644 --- a/src/apps/four_in_a_row.rs +++ b/src/apps/four_in_a_row.rs @@ -1106,11 +1106,12 @@ impl GameApp for FourInARowApp { sender_hash: &str, identity_id: &str, ) -> IncomingResult { - if command != CMD_ERROR - && let Err(message) = + if command != CMD_ERROR { + if let Err(message) = self.validate_wire_payload(session_id, command, payload, identity_id) - { - return error_result(ERR_PROTOCOL_ERROR, &message); + { + return error_result(ERR_PROTOCOL_ERROR, &message); + } } match command { CMD_CHALLENGE => self.handle_challenge_in(session_id, sender_hash, identity_id), diff --git a/src/apps/tictactoe.rs b/src/apps/tictactoe.rs index 9b57062..47ae1f1 100644 --- a/src/apps/tictactoe.rs +++ b/src/apps/tictactoe.rs @@ -758,11 +758,11 @@ impl TicTacToeApp { } fn handle_draw_offer_out(&self, session_id: &str, identity_id: &str) -> OutgoingResult { - if let Some(mut session) = self.get_session(session_id, identity_id) - && SessionStateMachine::apply_command(&mut session, CMD_DRAW_OFFER, false).is_ok() - { - session.set_draw_offer(identity_id); - self.save_session(&session); + if let Some(mut session) = self.get_session(session_id, identity_id) { + if SessionStateMachine::apply_command(&mut session, CMD_DRAW_OFFER, false).is_ok() { + session.set_draw_offer(identity_id); + self.save_session(&session); + } } OutgoingResult { payload: HashMap::new(), @@ -786,11 +786,11 @@ impl TicTacToeApp { } fn handle_draw_decline_out(&self, session_id: &str, identity_id: &str) -> OutgoingResult { - if let Some(mut session) = self.get_session(session_id, identity_id) - && SessionStateMachine::apply_command(&mut session, CMD_DRAW_DECLINE, false).is_ok() - { - session.clear_draw_offer(); - self.save_session(&session); + if let Some(mut session) = self.get_session(session_id, identity_id) { + if SessionStateMachine::apply_command(&mut session, CMD_DRAW_DECLINE, false).is_ok() { + session.clear_draw_offer(); + self.save_session(&session); + } } OutgoingResult { payload: HashMap::new(), @@ -1051,11 +1051,12 @@ impl GameApp for TicTacToeApp { sender_hash: &str, identity_id: &str, ) -> IncomingResult { - if command != CMD_ERROR - && let Err(message) = + if command != CMD_ERROR { + if let Err(message) = self.validate_incoming_payload(session_id, command, payload, identity_id) - { - return error_result(ERR_PROTOCOL_ERROR, &message); + { + return error_result(ERR_PROTOCOL_ERROR, &message); + } } match command { CMD_CHALLENGE => { @@ -1270,14 +1271,13 @@ impl GameApp for TicTacToeApp { // Pre-owner persisted records and stray owner metadata cannot // safely authorize a response. session.clear_draw_offer(); - } else if let Some(owner) = session.draw_offered_by() - && owner != session.identity_id - && owner != session.contact_hash - { - return Err(LrgpError::Validation { - code: ERR_PROTOCOL_ERROR.into(), - message: "draw offer owner is not a bound participant".into(), - }); + } else if let Some(owner) = session.draw_offered_by() { + if owner != session.identity_id && owner != session.contact_hash { + return Err(LrgpError::Validation { + code: ERR_PROTOCOL_ERROR.into(), + message: "draw offer owner is not a bound participant".into(), + }); + } } self.save_session(&session); Ok(()) diff --git a/src/dedup.rs b/src/dedup.rs index a0fc5b4..ac69725 100644 --- a/src/dedup.rs +++ b/src/dedup.rs @@ -234,10 +234,10 @@ impl ReplayDedup { pub fn forget_scoped_nonce(&mut self, identity_id: &str, session_id: &str, nonce: &[u8]) { let key = (identity_id.to_string(), session_id.to_string()); - if let Some(cache) = self.by_session.get_mut(&key) - && let Some(position) = cache.position(nonce) - { - cache.entries.remove(position); + if let Some(cache) = self.by_session.get_mut(&key) { + if let Some(position) = cache.position(nonce) { + cache.entries.remove(position); + } } if self .by_session diff --git a/src/router.rs b/src/router.rs index 3e8db96..cb3369d 100644 --- a/src/router.rs +++ b/src/router.rs @@ -135,12 +135,14 @@ impl LrgpRouter { return Ok(IncomingDispatch::Replay); } - if validated.command == CMD_CHALLENGE - && let Some((owner_app, _)) = + if validated.command == CMD_CHALLENGE { + if let Some((owner_app, _)) = self.find_session_owner(&validated.session_id, identity_id) - && owner_app != validated.app_id - { - return Err(LrgpError::SessionExists(validated.session_id)); + { + if owner_app != validated.app_id { + return Err(LrgpError::SessionExists(validated.session_id)); + } + } } if validated.command == CMD_ERROR { @@ -319,12 +321,12 @@ impl LrgpRouter { if session.contact_hash.is_empty() { return Err(LrgpError::ParticipantRequired); } - if let Some(recipient) = recipient_hash - && recipient != session.contact_hash - { - return Err(LrgpError::UnauthorizedPeer { - session_id: session.session_id, - }); + if let Some(recipient) = recipient_hash { + if recipient != session.contact_hash { + return Err(LrgpError::UnauthorizedPeer { + session_id: session.session_id, + }); + } } if command != CMD_ERROR { @@ -362,15 +364,15 @@ impl LrgpRouter { app.handle_outgoing(&effective_session_id, command, payload, identity_id) }; - if command == CMD_CHALLENGE - && let Err(error) = app.bind_session_peer( + if command == CMD_CHALLENGE { + if let Err(error) = app.bind_session_peer( &effective_session_id, identity_id, recipient_hash.expect("challenge recipient checked above"), - ) - { - app.rollback_session(&effective_session_id, identity_id, snapshot); - return Err(error); + ) { + app.rollback_session(&effective_session_id, identity_id, snapshot); + return Err(error); + } } let final_envelope = envelope::pack_envelope( @@ -501,9 +503,10 @@ impl LrgpRouter { let _creation_guard = self.session_creation.lock().unwrap(); if let Some((owner_app, _)) = self.find_session_owner(&session.session_id, &session.identity_id) - && owner_app != session.app_id { - return Err(LrgpError::SessionExists(session.session_id)); + if owner_app != session.app_id { + return Err(LrgpError::SessionExists(session.session_id)); + } } app.upsert_session(session) }