diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b53b8..08bdeea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - **[FIX]** Improve rust-analyzer resilience: when `#[nutype(...)]` arguments fail to parse (e.g. while still being typed), emit a best-effort type skeleton alongside the error so the newtype stays resolvable and downstream completions keep working (see [#178](https://github.com/greyblake/nutype/issues/178)). - **[FIX]** Correct the validation error `Display` message for float newtypes: `less` now reads "The value must be less than ..." and `less_or_equal` reads "The value must be less or equal to ..." (the two were previously swapped). Integer and decimal were already correct. - **[INTERNAL]** Consolidate the integer, float and decimal backends onto a shared numeric code-generation and validation layer (`common/generate/numeric.rs` and shared helpers in `common/validate.rs`), removing a large amount of duplicated code. No change to generated code or public API. +- **[IMPROVEMENT]** Mark the generated `new`, `try_new` and `new_unchecked` constructors with `#[inline]` so they can be inlined across crate boundaries, matching the existing `into_inner` (see [#237](https://github.com/greyblake/nutype/issues/237)). ### v0.7.0 - 2026-04-25 - **[BREAKING]** Rename `derive_unsafe` to `derive_unchecked` (both the feature flag and the attribute). diff --git a/nutype_macros/src/any/generate/mod.rs b/nutype_macros/src/any/generate/mod.rs index c719e59..5805292 100644 --- a/nutype_macros/src/any/generate/mod.rs +++ b/nutype_macros/src/any/generate/mod.rs @@ -162,3 +162,66 @@ impl GenerateNewtype for AnyNewtype { } } } + +#[cfg(test)] +mod inline_tests { + use super::*; + use crate::common::generate::gen_impl_into_inner; + use crate::common::models::{ConstructorVisibility, ErrorTypePath, Validation}; + use quote::{format_ident, quote}; + use syn::parse::Parser; + + fn inner_type() -> AnyInnerType { + let field = syn::Field::parse_unnamed + .parse2(quote!(String)) + .expect("field should parse"); + AnyInnerType::new(field) + } + + fn stripped(ts: TokenStream) -> String { + ts.to_string().split_whitespace().collect() + } + + #[test] + fn new_is_marked_inline() { + let rendered = stripped(AnyNewtype::gen_new( + &TypeName::new(format_ident!("Foo")), + &Generics::default(), + &inner_type(), + &[], + ConstFn::NoConst, + &ConstructorVisibility::Public, + )); + assert!(rendered.contains("#[inline]pubfnnew"), "{rendered}"); + } + + #[test] + fn try_new_is_marked_inline() { + let error_path: syn::Path = syn::parse_quote!(FooError); + let validation = Validation::Standard { + validators: Vec::::new(), + error_type_path: ErrorTypePath::new(error_path), + }; + let rendered = stripped(AnyNewtype::gen_try_new( + &TypeName::new(format_ident!("Foo")), + &Generics::default(), + &inner_type(), + &[], + &validation, + ConstFn::NoConst, + &ConstructorVisibility::Public, + )); + assert!(rendered.contains("#[inline]pubfntry_new"), "{rendered}"); + } + + #[test] + fn into_inner_stays_inline() { + let rendered = stripped(gen_impl_into_inner( + &TypeName::new(format_ident!("Foo")), + &Generics::default(), + inner_type(), + ConstFn::NoConst, + )); + assert!(rendered.contains("#[inline]pubfninto_inner"), "{rendered}"); + } +} diff --git a/nutype_macros/src/common/generate/mod.rs b/nutype_macros/src/common/generate/mod.rs index 65fc419..cff67d8 100644 --- a/nutype_macros/src/common/generate/mod.rs +++ b/nutype_macros/src/common/generate/mod.rs @@ -287,6 +287,7 @@ pub trait GenerateNewtype { #maybe_generated_validation_error impl #impl_generics #type_name #type_generics #where_clause { + #[inline] #constructor_visibility #const_fn fn try_new(raw_value: #input_type) -> ::core::result::Result { #convert_raw_value_if_necessary @@ -340,6 +341,7 @@ pub trait GenerateNewtype { // } quote!( impl #impl_generics #type_name #type_generics #where_clause { + #[inline] #constructor_visibility #const_fn fn new(raw_value: #input_type) -> Self { #convert_raw_value_if_necessary Self(Self::__sanitize__(raw_value)) diff --git a/nutype_macros/src/common/generate/new_unchecked.rs b/nutype_macros/src/common/generate/new_unchecked.rs index 8af8cf9..ec9e66d 100644 --- a/nutype_macros/src/common/generate/new_unchecked.rs +++ b/nutype_macros/src/common/generate/new_unchecked.rs @@ -16,6 +16,7 @@ pub fn gen_new_unchecked( /// Creates a value of type skipping the sanitization and validation /// rules. Generally, you should avoid using `::new_unchecked()` without a real need. /// Use `::new()` instead when it's possible. + #[inline] #constructor_visibility #const_fn unsafe fn new_unchecked(inner_value: #inner_type) -> #type_name { #type_name(inner_value) } @@ -23,3 +24,40 @@ pub fn gen_new_unchecked( }, } } + +#[cfg(test)] +mod tests { + use super::*; + use quote::format_ident; + + #[test] + fn new_unchecked_is_marked_inline() { + let type_name = TypeName::new(format_ident!("Foo")); + let inner_type = quote!(String); + let rendered = gen_new_unchecked( + &type_name, + inner_type, + NewUnchecked::On, + ConstFn::NoConst, + &ConstructorVisibility::Public, + ) + .to_string(); + + let stripped: String = rendered.split_whitespace().collect(); + assert!(stripped.contains("#[inline]pubunsafefnnew_unchecked")); + } + + #[test] + fn off_generates_nothing() { + let type_name = TypeName::new(format_ident!("Foo")); + let rendered = gen_new_unchecked( + &type_name, + quote!(String), + NewUnchecked::Off, + ConstFn::NoConst, + &ConstructorVisibility::Public, + ) + .to_string(); + assert!(rendered.is_empty()); + } +}