|
| 1 | +//! Best-effort recovery used when `#[nutype(...)]` fails to expand. |
| 2 | +//! |
| 3 | +//! `nutype` consumes the annotated struct and re-emits a brand new |
| 4 | +//! `struct Name(Inner);`. When expansion fails (most commonly because the user |
| 5 | +//! is still *typing* the attribute arguments and they don't parse yet), the |
| 6 | +//! macro would otherwise emit only `compile_error!(...)`, which makes the type |
| 7 | +//! itself vanish from the compiler's and rust-analyzer's view. Every downstream |
| 8 | +//! `Name::...`, `let x: Name`, and field access then turns red, even though the |
| 9 | +//! only thing wrong is an unfinished attribute. |
| 10 | +//! |
| 11 | +//! To keep rust-analyzer resilient, the error path emits the skeleton produced |
| 12 | +//! here *alongside* the real `compile_error!`. The skeleton only needs to |
| 13 | +//! type-check; it does not enforce any invariants. |
| 14 | +
|
| 15 | +use proc_macro2::TokenStream; |
| 16 | +use quote::quote; |
| 17 | +use syn::{Data, DeriveInput, Fields}; |
| 18 | + |
| 19 | +/// Best-effort, type-checkable skeleton of the newtype. |
| 20 | +/// |
| 21 | +/// Returns `None` when we cannot even recover a single-field tuple struct from |
| 22 | +/// the input (in that case the caller emits only the compile error). |
| 23 | +pub fn fallback_skeleton(type_definition: &TokenStream) -> Option<TokenStream> { |
| 24 | + let input: DeriveInput = syn::parse2(type_definition.clone()).ok()?; |
| 25 | + |
| 26 | + let DeriveInput { |
| 27 | + vis, |
| 28 | + ident, |
| 29 | + generics, |
| 30 | + data, |
| 31 | + .. |
| 32 | + } = input; |
| 33 | + |
| 34 | + let data_struct = match data { |
| 35 | + Data::Struct(s) => s, |
| 36 | + _ => return None, |
| 37 | + }; |
| 38 | + |
| 39 | + let inner_ty = match data_struct.fields { |
| 40 | + Fields::Unnamed(fields) => fields.unnamed.into_iter().next()?.ty, |
| 41 | + _ => return None, |
| 42 | + }; |
| 43 | + |
| 44 | + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); |
| 45 | + |
| 46 | + Some(quote! { |
| 47 | + // NB: the inner field visibility is intentionally dropped. The real |
| 48 | + // expansion forbids a visible inner field; the skeleton only needs to |
| 49 | + // type-check, not to enforce that invariant. |
| 50 | + #vis struct #ident #generics (#inner_ty) #where_clause; |
| 51 | + |
| 52 | + // Stub the inherent methods nutype always generates (the constructors and |
| 53 | + // `into_inner`) so that completion and resolution on `Name::...` keep |
| 54 | + // working while the attribute is broken. We can't know whether the real |
| 55 | + // type will end up validated (`try_new`) or not (`new`), so we provide |
| 56 | + // both. `unimplemented!()` diverges and coerces to any return type, the |
| 57 | + // argument is taken via `impl Into<Inner>` (every type satisfies the |
| 58 | + // reflexive `Inner: Into<Inner>`), and no extra type is introduced, so |
| 59 | + // this type-checks for every inner type without polluting the namespace. |
| 60 | + #[doc(hidden)] |
| 61 | + #[allow(dead_code, unused_variables, clippy::all)] |
| 62 | + impl #impl_generics #ident #ty_generics #where_clause { |
| 63 | + pub fn new(raw_value: impl ::core::convert::Into<#inner_ty>) -> Self { |
| 64 | + ::core::unimplemented!() |
| 65 | + } |
| 66 | + |
| 67 | + pub fn try_new( |
| 68 | + raw_value: impl ::core::convert::Into<#inner_ty>, |
| 69 | + ) -> ::core::result::Result<Self, ::core::convert::Infallible> { |
| 70 | + ::core::unimplemented!() |
| 71 | + } |
| 72 | + |
| 73 | + pub fn into_inner(self) -> #inner_ty { |
| 74 | + ::core::unimplemented!() |
| 75 | + } |
| 76 | + } |
| 77 | + }) |
| 78 | +} |
| 79 | + |
| 80 | +#[cfg(test)] |
| 81 | +mod tests { |
| 82 | + use super::*; |
| 83 | + |
| 84 | + fn renders(ts: &TokenStream) -> String { |
| 85 | + ts.to_string() |
| 86 | + } |
| 87 | + |
| 88 | + #[test] |
| 89 | + fn skeleton_for_simple_string_newtype() { |
| 90 | + let input = quote! { pub struct Foo(String); }; |
| 91 | + let out = fallback_skeleton(&input).expect("should produce a skeleton"); |
| 92 | + |
| 93 | + // The output must be valid Rust. |
| 94 | + syn::parse2::<syn::File>(out.clone()).expect("skeleton must parse as a file"); |
| 95 | + |
| 96 | + let rendered = renders(&out); |
| 97 | + assert!(rendered.contains("struct Foo")); |
| 98 | + assert!(rendered.contains("String")); |
| 99 | + // All the inherent methods nutype always generates are stubbed so that |
| 100 | + // `Foo::new(..)`, `Foo::try_new(..)` and `foo.into_inner()` keep |
| 101 | + // resolving while the attribute is broken. |
| 102 | + assert!(rendered.contains("fn new")); |
| 103 | + assert!(rendered.contains("fn try_new")); |
| 104 | + assert!(rendered.contains("fn into_inner")); |
| 105 | + } |
| 106 | + |
| 107 | + #[test] |
| 108 | + fn skeleton_preserves_generics_and_where_clause() { |
| 109 | + let input = quote! { pub struct Wrapper<T: Clone>(T) where T: Default; }; |
| 110 | + let out = fallback_skeleton(&input).expect("should produce a skeleton"); |
| 111 | + |
| 112 | + syn::parse2::<syn::File>(out.clone()).expect("skeleton must parse as a file"); |
| 113 | + |
| 114 | + let rendered = renders(&out); |
| 115 | + assert!(rendered.contains("struct Wrapper")); |
| 116 | + assert!(rendered.contains("Clone")); |
| 117 | + assert!(rendered.contains("Default")); |
| 118 | + } |
| 119 | + |
| 120 | + #[test] |
| 121 | + fn no_skeleton_for_named_struct() { |
| 122 | + let input = quote! { struct S { x: u8 } }; |
| 123 | + assert!(fallback_skeleton(&input).is_none()); |
| 124 | + } |
| 125 | + |
| 126 | + #[test] |
| 127 | + fn no_skeleton_for_enum() { |
| 128 | + let input = quote! { enum E { A, B } }; |
| 129 | + assert!(fallback_skeleton(&input).is_none()); |
| 130 | + } |
| 131 | + |
| 132 | + #[test] |
| 133 | + fn no_skeleton_for_empty_tuple_struct() { |
| 134 | + let input = quote! { struct S(); }; |
| 135 | + assert!(fallback_skeleton(&input).is_none()); |
| 136 | + } |
| 137 | + |
| 138 | + #[test] |
| 139 | + fn no_skeleton_for_unparsable_input() { |
| 140 | + let input = quote! { this is not a struct @ # !; }; |
| 141 | + assert!(fallback_skeleton(&input).is_none()); |
| 142 | + } |
| 143 | +} |
0 commit comments