Skip to content
Merged

173 #174

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.24.19] - 2025-10-12

### Fixed
- Updated macro code generation in `masterror-derive` to emit shorthand field
patterns (`field` instead of `field: field`) when the field name matches the
binding identifier, ensuring compatibility with Rust 2024 edition's
`non_shorthand_field_patterns` lint.
- Modified pattern generation in `error_trait.rs` for `source`, `backtrace`,
and `provide` implementations to conditionally use shorthand syntax,
eliminating redundant field-to-binding mappings that trigger warnings under
the new edition.
- Fixed race condition in `telemetry_flushes_after_subscriber_install` test by
moving error construction inside the dispatcher scope and calling
`rebuild_interest_cache()` before logging, ensuring the tracing subscriber
registers interest before event emission.

### Added
- Comprehensive `rust_2024_edition` integration test suite covering struct and
enum error types with `#[source]` attributes, validating that generated code
passes under `#![deny(non_shorthand_field_patterns)]`.
- Deny directive `#![deny(non_shorthand_field_patterns)]` in existing
`error_derive` test to enforce compliance and prevent future regressions.

### Changed
- Pattern generation logic now checks if field identifiers match binding names
before deciding between shorthand (`field`) and explicit (`field: binding`)
syntax, maintaining backward compatibility while adhering to Rust 2024
edition requirements.

### Why This Matters
Rust 2024 edition introduced the `non_shorthand_field_patterns` lint to
encourage cleaner, more idiomatic pattern matching. Without this fix, code
using `#[derive(Error)]` with `#[source]` attributes would trigger compiler
warnings (or errors with `-D warnings`) when upgrading to edition 2024,
breaking existing projects that rely on strict lint enforcement. This release
ensures seamless adoption of Rust 2024 edition for all `masterror` users.

## [0.24.18] - 2025-10-09

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[package]
name = "masterror"
version = "0.24.18"
version = "0.24.19"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ The build script keeps the full feature snippet below in sync with

~~~toml
[dependencies]
masterror = { version = "0.24.18", default-features = false }
masterror = { version = "0.24.19", default-features = false }
# or with features:
# masterror = { version = "0.24.18", features = [
# masterror = { version = "0.24.19", features = [
# "std", "axum", "actix", "openapi",
# "serde_json", "tracing", "metrics", "backtrace",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
Expand Down Expand Up @@ -459,4 +459,3 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");

MSRV: **1.90** · License: **MIT OR Apache-2.0** · No `unsafe`


30 changes: 23 additions & 7 deletions masterror-derive/src/error_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,14 @@ fn variant_transparent_source(variant: &VariantData) -> TokenStream {
match &variant.fields {
Fields::Unit => quote! { Self::#variant_ident => None },
Fields::Named(fields) => {
let binding = fields[0].ident.clone().expect("named field");
let field_ident = fields[0].ident.clone().expect("named field");
let pattern = if fields.len() == 1 {
quote!(Self::#variant_ident { #binding })
quote!(Self::#variant_ident { #field_ident })
} else {
quote!(Self::#variant_ident { #binding, .. })
quote!(Self::#variant_ident { #field_ident, .. })
};
quote! {
#pattern => std::error::Error::source(#binding)
#pattern => std::error::Error::source(#field_ident)
}
}
Fields::Unnamed(fields) => {
Expand Down Expand Up @@ -162,7 +162,13 @@ fn variant_template_source(variant: &VariantData) -> TokenStream {
(Fields::Named(fields), Some(field)) => {
let field_ident = field.ident.clone().expect("named field");
let binding = binding_ident(field);
let pattern = if fields.len() == 1 {
let pattern = if field_ident == binding {
if fields.len() == 1 {
quote!(Self::#variant_ident { #field_ident })
} else {
quote!(Self::#variant_ident { #field_ident, .. })
}
} else if fields.len() == 1 {
quote!(Self::#variant_ident { #field_ident: #binding })
} else {
quote!(Self::#variant_ident { #field_ident: #binding, .. })
Expand Down Expand Up @@ -253,7 +259,13 @@ fn variant_backtrace_arm(variant: &VariantData) -> TokenStream {
let field = backtrace.field();
let field_ident = field.ident.clone().expect("named field");
let binding = binding_ident(field);
let pattern = if fields.len() == 1 {
let pattern = if field_ident == binding {
if fields.len() == 1 {
quote!(Self::#variant_ident { #field_ident })
} else {
quote!(Self::#variant_ident { #field_ident, .. })
}
} else if fields.len() == 1 {
quote!(Self::#variant_ident { #field_ident: #binding })
} else {
quote!(Self::#variant_ident { #field_ident: #binding, .. })
Expand Down Expand Up @@ -488,7 +500,11 @@ fn variant_provide_named_arm(
if needs_binding {
let binding = binding_ident(field);
let pattern_binding = binding.clone();
entries.push(quote!(#ident: #pattern_binding));
if ident == pattern_binding {
entries.push(quote!(#ident));
} else {
entries.push(quote!(#ident: #pattern_binding));
}

if backtrace.is_some_and(|candidate| candidate.index() == field.index) {
backtrace_binding = Some(binding.clone());
Expand Down
26 changes: 13 additions & 13 deletions src/app_error/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,26 +619,26 @@ fn telemetry_flushes_after_subscriber_install() {
let _guard = TELEMETRY_GUARD.lock().expect("telemetry guard");

use telemetry_support::new_recording_dispatch;
use tracing::dispatcher;

let err = AppError::internal("boom");
use tracing::{callsite::rebuild_interest_cache, dispatcher};

let (dispatch, events) = new_recording_dispatch();
let events = events.clone();

dispatcher::with_default(&dispatch, || {
rebuild_interest_cache();
let err = AppError::internal("boom");
err.log();
});

let events = events.lock().expect("events lock");
assert_eq!(
events.len(),
1,
"expected telemetry after subscriber install"
);
let event = &events[0];
assert_eq!(event.code.as_deref(), Some(AppCode::Internal.as_str()));
assert_eq!(event.category.as_deref(), Some("Internal"));
let events = events.lock().expect("events lock");
assert_eq!(
events.len(),
1,
"expected telemetry after subscriber install"
);
let event = &events[0];
assert_eq!(event.code.as_deref(), Some(AppCode::Internal.as_str()));
assert_eq!(event.category.as_deref(), Some("Internal"));
});
}

#[cfg(feature = "metrics")]
Expand Down
3 changes: 2 additions & 1 deletion tests/error_derive.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![allow(unused_variables, non_shorthand_field_patterns)]
#![allow(unused_variables)]
#![deny(non_shorthand_field_patterns)]

// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
//
Expand Down
106 changes: 106 additions & 0 deletions tests/rust_2024_edition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
//
// SPDX-License-Identifier: MIT

//! Test for Rust 2024 edition compatibility
//!
//! This test ensures that the macro-generated code does not trigger the
//! `non_shorthand_field_patterns` lint introduced in Rust 2024 edition.

#![deny(non_shorthand_field_patterns)]

use std::error::Error as StdError;

use masterror::Error;

#[derive(Debug, Error)]
#[error("parse error: {source}")]
pub struct ParseError {
#[source]
source: std::num::ParseIntError
}

#[derive(Debug, Error)]
#[error("io error")]
pub struct IoError {
#[source]
source: std::io::Error
}

#[derive(Debug, Error)]
pub enum AppError {
#[error("failed to parse: {source}")]
Parse {
#[source]
source: std::num::ParseIntError
},
#[error("io failure: {source}")]
Io {
#[source]
source: std::io::Error
},
#[error("network error: {0}")]
Network(#[source] std::io::Error),
#[error("unknown error")]
Unknown
}

#[derive(Debug, Error)]
#[error("multi-field error: {message}, context: {context:?}")]
pub struct MultiFieldError {
message: String,
#[source]
source: std::io::Error,
context: Option<String>
}

#[derive(Debug, Error)]
pub enum ComplexError {
#[error("complex variant: {message}, code: {code}, caused by: {source}")]
Complex {
message: String,
#[source]
source: std::io::Error,
code: u16
}
}

#[test]
fn test_struct_with_source() {
let inner = "not a number".parse::<i32>().unwrap_err();
let error = ParseError {
source: inner
};
assert!(error.source().is_some());
}

#[test]
fn test_enum_with_source() {
let inner = "not a number".parse::<i32>().unwrap_err();
let error = AppError::Parse {
source: inner
};
assert!(error.source().is_some());
}

#[test]
fn test_multi_field_struct() {
let io_error = std::io::Error::other("test");
let error = MultiFieldError {
message: "test message".to_string(),
source: io_error,
context: Some("additional context".to_string())
};
assert!(error.source().is_some());
}

#[test]
fn test_complex_enum_variant() {
let io_error = std::io::Error::other("test");
let error = ComplexError::Complex {
message: "test".to_string(),
source: io_error,
code: 500
};
assert!(error.source().is_some());
}