Skip to content

Commit 2ff468e

Browse files
authored
Merge pull request #258 from ChrisJr404/inline-constructors
Mark generated constructors as #[inline]
2 parents d592cbb + bcf3af9 commit 2ff468e

4 files changed

Lines changed: 104 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- **[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)).
1010
- **[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.
1111
- **[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.
12+
- **[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)).
1213

1314
### v0.7.0 - 2026-04-25
1415
- **[BREAKING]** Rename `derive_unsafe` to `derive_unchecked` (both the feature flag and the attribute).

nutype_macros/src/any/generate/mod.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,66 @@ impl GenerateNewtype for AnyNewtype {
162162
}
163163
}
164164
}
165+
166+
#[cfg(test)]
167+
mod inline_tests {
168+
use super::*;
169+
use crate::common::generate::gen_impl_into_inner;
170+
use crate::common::models::{ConstructorVisibility, ErrorTypePath, Validation};
171+
use quote::{format_ident, quote};
172+
use syn::parse::Parser;
173+
174+
fn inner_type() -> AnyInnerType {
175+
let field = syn::Field::parse_unnamed
176+
.parse2(quote!(String))
177+
.expect("field should parse");
178+
AnyInnerType::new(field)
179+
}
180+
181+
fn stripped(ts: TokenStream) -> String {
182+
ts.to_string().split_whitespace().collect()
183+
}
184+
185+
#[test]
186+
fn new_is_marked_inline() {
187+
let rendered = stripped(AnyNewtype::gen_new(
188+
&TypeName::new(format_ident!("Foo")),
189+
&Generics::default(),
190+
&inner_type(),
191+
&[],
192+
ConstFn::NoConst,
193+
&ConstructorVisibility::Public,
194+
));
195+
assert!(rendered.contains("#[inline]pubfnnew"), "{rendered}");
196+
}
197+
198+
#[test]
199+
fn try_new_is_marked_inline() {
200+
let error_path: syn::Path = syn::parse_quote!(FooError);
201+
let validation = Validation::Standard {
202+
validators: Vec::<AnyValidator>::new(),
203+
error_type_path: ErrorTypePath::new(error_path),
204+
};
205+
let rendered = stripped(AnyNewtype::gen_try_new(
206+
&TypeName::new(format_ident!("Foo")),
207+
&Generics::default(),
208+
&inner_type(),
209+
&[],
210+
&validation,
211+
ConstFn::NoConst,
212+
&ConstructorVisibility::Public,
213+
));
214+
assert!(rendered.contains("#[inline]pubfntry_new"), "{rendered}");
215+
}
216+
217+
#[test]
218+
fn into_inner_stays_inline() {
219+
let rendered = stripped(gen_impl_into_inner(
220+
&TypeName::new(format_ident!("Foo")),
221+
&Generics::default(),
222+
inner_type(),
223+
ConstFn::NoConst,
224+
));
225+
assert!(rendered.contains("#[inline]pubfninto_inner"), "{rendered}");
226+
}
227+
}

nutype_macros/src/common/generate/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ pub trait GenerateNewtype {
287287
#maybe_generated_validation_error
288288

289289
impl #impl_generics #type_name #type_generics #where_clause {
290+
#[inline]
290291
#constructor_visibility #const_fn fn try_new(raw_value: #input_type) -> ::core::result::Result<Self, #error_type_path> {
291292
#convert_raw_value_if_necessary
292293

@@ -340,6 +341,7 @@ pub trait GenerateNewtype {
340341
// }
341342
quote!(
342343
impl #impl_generics #type_name #type_generics #where_clause {
344+
#[inline]
343345
#constructor_visibility #const_fn fn new(raw_value: #input_type) -> Self {
344346
#convert_raw_value_if_necessary
345347
Self(Self::__sanitize__(raw_value))

nutype_macros/src/common/generate/new_unchecked.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,48 @@ pub fn gen_new_unchecked(
1616
/// Creates a value of type skipping the sanitization and validation
1717
/// rules. Generally, you should avoid using `::new_unchecked()` without a real need.
1818
/// Use `::new()` instead when it's possible.
19+
#[inline]
1920
#constructor_visibility #const_fn unsafe fn new_unchecked(inner_value: #inner_type) -> #type_name {
2021
#type_name(inner_value)
2122
}
2223
}
2324
},
2425
}
2526
}
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use super::*;
31+
use quote::format_ident;
32+
33+
#[test]
34+
fn new_unchecked_is_marked_inline() {
35+
let type_name = TypeName::new(format_ident!("Foo"));
36+
let inner_type = quote!(String);
37+
let rendered = gen_new_unchecked(
38+
&type_name,
39+
inner_type,
40+
NewUnchecked::On,
41+
ConstFn::NoConst,
42+
&ConstructorVisibility::Public,
43+
)
44+
.to_string();
45+
46+
let stripped: String = rendered.split_whitespace().collect();
47+
assert!(stripped.contains("#[inline]pubunsafefnnew_unchecked"));
48+
}
49+
50+
#[test]
51+
fn off_generates_nothing() {
52+
let type_name = TypeName::new(format_ident!("Foo"));
53+
let rendered = gen_new_unchecked(
54+
&type_name,
55+
quote!(String),
56+
NewUnchecked::Off,
57+
ConstFn::NoConst,
58+
&ConstructorVisibility::Public,
59+
)
60+
.to_string();
61+
assert!(rendered.is_empty());
62+
}
63+
}

0 commit comments

Comments
 (0)