Skip to content

Commit 355faaa

Browse files
authored
173 (#174)
* #173 fix: Rust 2024 edition compatibility for non_shorthand_field_patterns This commit resolves issue #173 by updating the macro code generation to use shorthand field pattern syntax required by Rust 2024 edition. Changes: - Modified error_trait.rs to generate shorthand patterns (field vs field: field) - Added deny directive for non_shorthand_field_patterns in error_derive test - Created new rust_2024_edition.rs test with comprehensive coverage - Fixed telemetry_flushes_after_subscriber_install test race condition All tests pass successfully with Rust 2024 edition. * #173 chore: bump version to 0.24.19 and update changelog
1 parent 37bc623 commit 355faaa

File tree

7 files changed

+184
-25
lines changed

7 files changed

+184
-25
lines changed

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,43 @@ All notable changes to this project will be documented in this file.
99

1010
## [Unreleased]
1111

12+
## [0.24.19] - 2025-10-12
13+
14+
### Fixed
15+
- Updated macro code generation in `masterror-derive` to emit shorthand field
16+
patterns (`field` instead of `field: field`) when the field name matches the
17+
binding identifier, ensuring compatibility with Rust 2024 edition's
18+
`non_shorthand_field_patterns` lint.
19+
- Modified pattern generation in `error_trait.rs` for `source`, `backtrace`,
20+
and `provide` implementations to conditionally use shorthand syntax,
21+
eliminating redundant field-to-binding mappings that trigger warnings under
22+
the new edition.
23+
- Fixed race condition in `telemetry_flushes_after_subscriber_install` test by
24+
moving error construction inside the dispatcher scope and calling
25+
`rebuild_interest_cache()` before logging, ensuring the tracing subscriber
26+
registers interest before event emission.
27+
28+
### Added
29+
- Comprehensive `rust_2024_edition` integration test suite covering struct and
30+
enum error types with `#[source]` attributes, validating that generated code
31+
passes under `#![deny(non_shorthand_field_patterns)]`.
32+
- Deny directive `#![deny(non_shorthand_field_patterns)]` in existing
33+
`error_derive` test to enforce compliance and prevent future regressions.
34+
35+
### Changed
36+
- Pattern generation logic now checks if field identifiers match binding names
37+
before deciding between shorthand (`field`) and explicit (`field: binding`)
38+
syntax, maintaining backward compatibility while adhering to Rust 2024
39+
edition requirements.
40+
41+
### Why This Matters
42+
Rust 2024 edition introduced the `non_shorthand_field_patterns` lint to
43+
encourage cleaner, more idiomatic pattern matching. Without this fix, code
44+
using `#[derive(Error)]` with `#[source]` attributes would trigger compiler
45+
warnings (or errors with `-D warnings`) when upgrading to edition 2024,
46+
breaking existing projects that rely on strict lint enforcement. This release
47+
ensures seamless adoption of Rust 2024 edition for all `masterror` users.
48+
1249
## [0.24.18] - 2025-10-09
1350

1451
### Fixed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
[package]
66
name = "masterror"
7-
version = "0.24.18"
7+
version = "0.24.19"
88
rust-version = "1.90"
99
edition = "2024"
1010
license = "MIT OR Apache-2.0"

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ The build script keeps the full feature snippet below in sync with
8080

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

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

462-

masterror-derive/src/error_trait.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ fn variant_transparent_source(variant: &VariantData) -> TokenStream {
115115
match &variant.fields {
116116
Fields::Unit => quote! { Self::#variant_ident => None },
117117
Fields::Named(fields) => {
118-
let binding = fields[0].ident.clone().expect("named field");
118+
let field_ident = fields[0].ident.clone().expect("named field");
119119
let pattern = if fields.len() == 1 {
120-
quote!(Self::#variant_ident { #binding })
120+
quote!(Self::#variant_ident { #field_ident })
121121
} else {
122-
quote!(Self::#variant_ident { #binding, .. })
122+
quote!(Self::#variant_ident { #field_ident, .. })
123123
};
124124
quote! {
125-
#pattern => std::error::Error::source(#binding)
125+
#pattern => std::error::Error::source(#field_ident)
126126
}
127127
}
128128
Fields::Unnamed(fields) => {
@@ -162,7 +162,13 @@ fn variant_template_source(variant: &VariantData) -> TokenStream {
162162
(Fields::Named(fields), Some(field)) => {
163163
let field_ident = field.ident.clone().expect("named field");
164164
let binding = binding_ident(field);
165-
let pattern = if fields.len() == 1 {
165+
let pattern = if field_ident == binding {
166+
if fields.len() == 1 {
167+
quote!(Self::#variant_ident { #field_ident })
168+
} else {
169+
quote!(Self::#variant_ident { #field_ident, .. })
170+
}
171+
} else if fields.len() == 1 {
166172
quote!(Self::#variant_ident { #field_ident: #binding })
167173
} else {
168174
quote!(Self::#variant_ident { #field_ident: #binding, .. })
@@ -253,7 +259,13 @@ fn variant_backtrace_arm(variant: &VariantData) -> TokenStream {
253259
let field = backtrace.field();
254260
let field_ident = field.ident.clone().expect("named field");
255261
let binding = binding_ident(field);
256-
let pattern = if fields.len() == 1 {
262+
let pattern = if field_ident == binding {
263+
if fields.len() == 1 {
264+
quote!(Self::#variant_ident { #field_ident })
265+
} else {
266+
quote!(Self::#variant_ident { #field_ident, .. })
267+
}
268+
} else if fields.len() == 1 {
257269
quote!(Self::#variant_ident { #field_ident: #binding })
258270
} else {
259271
quote!(Self::#variant_ident { #field_ident: #binding, .. })
@@ -488,7 +500,11 @@ fn variant_provide_named_arm(
488500
if needs_binding {
489501
let binding = binding_ident(field);
490502
let pattern_binding = binding.clone();
491-
entries.push(quote!(#ident: #pattern_binding));
503+
if ident == pattern_binding {
504+
entries.push(quote!(#ident));
505+
} else {
506+
entries.push(quote!(#ident: #pattern_binding));
507+
}
492508

493509
if backtrace.is_some_and(|candidate| candidate.index() == field.index) {
494510
backtrace_binding = Some(binding.clone());

src/app_error/tests.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -619,26 +619,26 @@ fn telemetry_flushes_after_subscriber_install() {
619619
let _guard = TELEMETRY_GUARD.lock().expect("telemetry guard");
620620

621621
use telemetry_support::new_recording_dispatch;
622-
use tracing::dispatcher;
623-
624-
let err = AppError::internal("boom");
622+
use tracing::{callsite::rebuild_interest_cache, dispatcher};
625623

626624
let (dispatch, events) = new_recording_dispatch();
627625
let events = events.clone();
628626

629627
dispatcher::with_default(&dispatch, || {
628+
rebuild_interest_cache();
629+
let err = AppError::internal("boom");
630630
err.log();
631-
});
632631

633-
let events = events.lock().expect("events lock");
634-
assert_eq!(
635-
events.len(),
636-
1,
637-
"expected telemetry after subscriber install"
638-
);
639-
let event = &events[0];
640-
assert_eq!(event.code.as_deref(), Some(AppCode::Internal.as_str()));
641-
assert_eq!(event.category.as_deref(), Some("Internal"));
632+
let events = events.lock().expect("events lock");
633+
assert_eq!(
634+
events.len(),
635+
1,
636+
"expected telemetry after subscriber install"
637+
);
638+
let event = &events[0];
639+
assert_eq!(event.code.as_deref(), Some(AppCode::Internal.as_str()));
640+
assert_eq!(event.category.as_deref(), Some("Internal"));
641+
});
642642
}
643643

644644
#[cfg(feature = "metrics")]

tests/error_derive.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
#![allow(unused_variables, non_shorthand_field_patterns)]
1+
#![allow(unused_variables)]
2+
#![deny(non_shorthand_field_patterns)]
23

34
// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
45
//

tests/rust_2024_edition.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
//! Test for Rust 2024 edition compatibility
6+
//!
7+
//! This test ensures that the macro-generated code does not trigger the
8+
//! `non_shorthand_field_patterns` lint introduced in Rust 2024 edition.
9+
10+
#![deny(non_shorthand_field_patterns)]
11+
12+
use std::error::Error as StdError;
13+
14+
use masterror::Error;
15+
16+
#[derive(Debug, Error)]
17+
#[error("parse error: {source}")]
18+
pub struct ParseError {
19+
#[source]
20+
source: std::num::ParseIntError
21+
}
22+
23+
#[derive(Debug, Error)]
24+
#[error("io error")]
25+
pub struct IoError {
26+
#[source]
27+
source: std::io::Error
28+
}
29+
30+
#[derive(Debug, Error)]
31+
pub enum AppError {
32+
#[error("failed to parse: {source}")]
33+
Parse {
34+
#[source]
35+
source: std::num::ParseIntError
36+
},
37+
#[error("io failure: {source}")]
38+
Io {
39+
#[source]
40+
source: std::io::Error
41+
},
42+
#[error("network error: {0}")]
43+
Network(#[source] std::io::Error),
44+
#[error("unknown error")]
45+
Unknown
46+
}
47+
48+
#[derive(Debug, Error)]
49+
#[error("multi-field error: {message}, context: {context:?}")]
50+
pub struct MultiFieldError {
51+
message: String,
52+
#[source]
53+
source: std::io::Error,
54+
context: Option<String>
55+
}
56+
57+
#[derive(Debug, Error)]
58+
pub enum ComplexError {
59+
#[error("complex variant: {message}, code: {code}, caused by: {source}")]
60+
Complex {
61+
message: String,
62+
#[source]
63+
source: std::io::Error,
64+
code: u16
65+
}
66+
}
67+
68+
#[test]
69+
fn test_struct_with_source() {
70+
let inner = "not a number".parse::<i32>().unwrap_err();
71+
let error = ParseError {
72+
source: inner
73+
};
74+
assert!(error.source().is_some());
75+
}
76+
77+
#[test]
78+
fn test_enum_with_source() {
79+
let inner = "not a number".parse::<i32>().unwrap_err();
80+
let error = AppError::Parse {
81+
source: inner
82+
};
83+
assert!(error.source().is_some());
84+
}
85+
86+
#[test]
87+
fn test_multi_field_struct() {
88+
let io_error = std::io::Error::other("test");
89+
let error = MultiFieldError {
90+
message: "test message".to_string(),
91+
source: io_error,
92+
context: Some("additional context".to_string())
93+
};
94+
assert!(error.source().is_some());
95+
}
96+
97+
#[test]
98+
fn test_complex_enum_variant() {
99+
let io_error = std::io::Error::other("test");
100+
let error = ComplexError::Complex {
101+
message: "test".to_string(),
102+
source: io_error,
103+
code: 500
104+
};
105+
assert!(error.source().is_some());
106+
}

0 commit comments

Comments
 (0)