@layerzerolabs/framework-solana-anchor-trait 0.2.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/Cargo.toml +18 -0
  2. package/examples/shapes/Anchor.toml +19 -0
  3. package/examples/shapes/macros/Cargo.toml +17 -0
  4. package/examples/shapes/macros/src/instructions/compute_area.rs +50 -0
  5. package/examples/shapes/macros/src/instructions/compute_perimeter.rs +50 -0
  6. package/examples/shapes/macros/src/instructions/get_dimension_count.rs +44 -0
  7. package/examples/shapes/macros/src/instructions/get_name.rs +18 -0
  8. package/examples/shapes/macros/src/instructions/initialize.rs +18 -0
  9. package/examples/shapes/macros/src/instructions/mod.rs +26 -0
  10. package/examples/shapes/macros/src/lib.rs +50 -0
  11. package/examples/shapes/macros/src/shapes.rs +56 -0
  12. package/examples/shapes/programs/ellipse/Cargo.toml +30 -0
  13. package/examples/shapes/programs/ellipse/src/lib.rs +88 -0
  14. package/examples/shapes/programs/ellipse/src/state.rs +9 -0
  15. package/examples/shapes/programs/triangle/Cargo.toml +30 -0
  16. package/examples/shapes/programs/triangle/src/lib.rs +83 -0
  17. package/examples/shapes/programs/triangle/src/state.rs +9 -0
  18. package/package.json +36 -0
  19. package/rust-toolchain.toml +3 -0
  20. package/rustfmt.toml +23 -0
  21. package/src/assembly.rs +63 -0
  22. package/src/generation.rs +226 -0
  23. package/src/lib.rs +126 -0
  24. package/src/overrides.rs +289 -0
  25. package/tests/assembly.rs +339 -0
  26. package/tests/common/mod.rs +206 -0
  27. package/tests/declare_instructions.rs +206 -0
  28. package/tests/generation.rs +358 -0
  29. package/tests/override.rs +414 -0
  30. package/tests/snapshots/snapshots__tests__snapshot_all_defaults.snap +35 -0
  31. package/tests/snapshots/snapshots__tests__snapshot_full_override.snap +47 -0
  32. package/tests/snapshots/snapshots__tests__snapshot_handler_override.snap +42 -0
  33. package/tests/snapshots/snapshots__tests__snapshot_mixed_overrides.snap +57 -0
  34. package/tests/snapshots.rs +139 -0
@@ -0,0 +1,289 @@
1
+ //! Instruction override collection and validation.
2
+ //!
3
+ //! Discovers `#[*_instruction]` handlers inside a module, validates them,
4
+ //! relocates them into wrapper modules, and provides filtered module items for
5
+ //! final assembly.
6
+
7
+ use itertools::Itertools;
8
+ use proc_macro2::TokenStream;
9
+ use quote::quote;
10
+ use std::collections::BTreeMap;
11
+ use syn::{spanned::Spanned, Attribute, Item, ItemFn, ItemMod, Type};
12
+
13
+ use crate::InstructionSet;
14
+
15
+ /// Static configuration for a macro crate's instruction override system.
16
+ #[derive(Debug, Clone, Copy)]
17
+ pub struct OverrideConfig {
18
+ /// Attribute name used to mark override handlers, e.g. `"oapp_instruction"`.
19
+ pub attr_name: &'static str,
20
+ /// Human-readable domain label for diagnostics, e.g. `"OApp"` or `"RBAC"`.
21
+ pub domain: &'static str,
22
+ }
23
+
24
+ /// Metadata captured for one overridden handler function.
25
+ pub struct HandlerMeta {
26
+ /// Wrapper module identifier for the relocated handler.
27
+ pub wrapper_mod: syn::Ident,
28
+ /// Original implementor-defined handler function identifier.
29
+ pub fn_ident: syn::Ident,
30
+ /// Inner type `T` extracted from `Context<...T>`.
31
+ pub accounts_type: Type,
32
+ /// The full `Context<...>` type as written (preserves lifetime args).
33
+ pub context_type: Type,
34
+ /// Function-level generics (e.g. `<'info>`) from the override handler.
35
+ pub fn_generics: syn::Generics,
36
+ }
37
+
38
+ /// Collection of custom instruction handlers discovered from a module.
39
+ pub struct InstructionOverrides<Ix: InstructionSet> {
40
+ handlers: BTreeMap<Ix, HandlerMeta>,
41
+ pub wrapper_defs: TokenStream,
42
+ pub config: OverrideConfig,
43
+ }
44
+
45
+ impl<Ix: InstructionSet> InstructionOverrides<Ix> {
46
+ /// Creates an empty override collection with the given config.
47
+ fn new(config: OverrideConfig) -> Self {
48
+ Self { handlers: BTreeMap::new(), wrapper_defs: TokenStream::new(), config }
49
+ }
50
+
51
+ /// Scans a module for `#[<attr>(...)]` handlers and builds the override collection.
52
+ pub fn collect(module: &ItemMod, config: OverrideConfig) -> syn::Result<Self> {
53
+ let mut collected = Self::new(config);
54
+
55
+ let items = match &module.content {
56
+ Some((_, items)) => items,
57
+ None => return Ok(collected),
58
+ };
59
+
60
+ // Walk every function in the module looking for override attributes.
61
+ for item in items {
62
+ let Item::Fn(fn_item) = item else { continue };
63
+ let Some(target) = parse_override_target::<Ix>(fn_item, config)? else {
64
+ continue;
65
+ };
66
+
67
+ // Reject duplicate overrides for the same instruction.
68
+ if collected.handlers.contains_key(&target) {
69
+ return Err(syn::Error::new_spanned(
70
+ &fn_item.sig.ident,
71
+ format!(
72
+ "duplicate #[{}] for instruction `{}` is not allowed",
73
+ config.attr_name,
74
+ target.canonical_name(),
75
+ ),
76
+ ));
77
+ }
78
+
79
+ // Relocate the handler into a wrapper module and record its metadata.
80
+ let meta = collected.wrap_handler(target, fn_item)?;
81
+ collected.handlers.insert(target, meta);
82
+ }
83
+
84
+ // Ensure no plain `fn name()` shadows a known instruction name.
85
+ collected.reject_name_collisions(module)?;
86
+ Ok(collected)
87
+ }
88
+
89
+ /// Returns metadata for an overridden instruction, if present.
90
+ pub fn get(&self, target: Ix) -> Option<&HandlerMeta> {
91
+ self.handlers.get(&target)
92
+ }
93
+
94
+ /// Returns module items with relocated override handlers removed.
95
+ pub fn filtered_items(&self, module: &ItemMod) -> TokenStream {
96
+ let Some((_, items)) = &module.content else {
97
+ return TokenStream::new();
98
+ };
99
+
100
+ let kept: Vec<_> = items
101
+ .iter()
102
+ .filter(|item| match item {
103
+ Item::Fn(fn_item) => !self.is_overridden(&fn_item.sig.ident),
104
+ _ => true,
105
+ })
106
+ .collect();
107
+
108
+ quote!(#(#kept)*)
109
+ }
110
+
111
+ /// Checks whether a function ident belongs to an override handler.
112
+ fn is_overridden(&self, fn_ident: &syn::Ident) -> bool {
113
+ self.handlers.values().any(|h| h.fn_ident == *fn_ident)
114
+ }
115
+
116
+ /// Relocates a handler function into an isolated wrapper module
117
+ /// and returns the corresponding [`HandlerMeta`].
118
+ fn wrap_handler(&mut self, target: Ix, fn_item: &ItemFn) -> syn::Result<HandlerMeta> {
119
+ let (accounts_type, context_type) = infer_context_type(fn_item)?;
120
+ let fn_ident = fn_item.sig.ident.clone();
121
+ let fn_generics = fn_item.sig.generics.clone();
122
+
123
+ // Generate a deterministic module name: `__<attr>__<instruction>_handler`.
124
+ let wrapper_mod = syn::Ident::new(
125
+ &format!("__{}__{}_handler", self.config.attr_name, target.canonical_name()),
126
+ proc_macro2::Span::call_site(),
127
+ );
128
+
129
+ // Clone the function without the instruction attribute.
130
+ let wrapped_fn = strip_override_attr(fn_item, self.config)?;
131
+
132
+ // Emit the wrapper module so the handler lives in its own scope.
133
+ self.wrapper_defs.extend(quote! {
134
+ mod #wrapper_mod {
135
+ use super::*;
136
+ #wrapped_fn
137
+ }
138
+ });
139
+
140
+ Ok(HandlerMeta { wrapper_mod, fn_ident, accounts_type, context_type, fn_generics })
141
+ }
142
+
143
+ /// Rejects unattributed functions whose name collides with a known instruction.
144
+ ///
145
+ /// A bare `fn lz_receive(...)` without `#[oapp_instruction]`
146
+ /// would silently shadow the generated entrypoint — catch that early.
147
+ fn reject_name_collisions(&self, module: &ItemMod) -> syn::Result<()> {
148
+ let Some((_, items)) = &module.content else {
149
+ return Ok(());
150
+ };
151
+
152
+ for &ix in Ix::all() {
153
+ let name = ix.canonical_name();
154
+ let collides = items.iter().any(|item| match item {
155
+ Item::Fn(f) => f.sig.ident == name && !has_override_attr(f, self.config),
156
+ _ => false,
157
+ });
158
+
159
+ if collides {
160
+ return Err(syn::Error::new(
161
+ module.span(),
162
+ format!(
163
+ "`fn {name}(...)` conflicts with {domain} instruction `{name}`: \
164
+ use #[{attr}] to override it",
165
+ domain = self.config.domain,
166
+ attr = self.config.attr_name,
167
+ ),
168
+ ));
169
+ }
170
+ }
171
+ Ok(())
172
+ }
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Helper functions
177
+ // ---------------------------------------------------------------------------
178
+
179
+ /// Returns `true` if the function carries the override attribute.
180
+ fn has_override_attr(fn_item: &ItemFn, cfg: OverrideConfig) -> bool {
181
+ fn_item.attrs.iter().any(|attr| is_override_attr(attr, cfg))
182
+ }
183
+
184
+ /// Returns `true` if `attr`'s last path segment matches the configured attribute name.
185
+ fn is_override_attr(attr: &Attribute, cfg: OverrideConfig) -> bool {
186
+ attr.path().segments.last().is_some_and(|seg| seg.ident == cfg.attr_name)
187
+ }
188
+
189
+ /// Returns a clone of `fn_item` with the override attribute removed.
190
+ fn strip_override_attr(fn_item: &ItemFn, cfg: OverrideConfig) -> syn::Result<ItemFn> {
191
+ let mut cleaned = fn_item.clone();
192
+ let before = cleaned.attrs.len();
193
+ cleaned.attrs.retain(|attr| !is_override_attr(attr, cfg));
194
+
195
+ if cleaned.attrs.len() == before {
196
+ return Err(syn::Error::new_spanned(
197
+ fn_item,
198
+ format!("internal error: expected #[{}] attribute on handler function", cfg.attr_name),
199
+ ));
200
+ }
201
+ Ok(cleaned)
202
+ }
203
+
204
+ /// Parses the override target from a function's override attribute.
205
+ ///
206
+ /// The instruction name is inferred from the handler's function name
207
+ /// (e.g. `fn set_paused(...)` → instruction `set_paused`).
208
+ /// The attribute must not contain any arguments.
209
+ ///
210
+ /// Returns `Ok(None)` if the function has no such attribute,
211
+ /// `Ok(Some(ix))` on success, or `Err` on malformed attributes.
212
+ fn parse_override_target<Ix: InstructionSet>(
213
+ fn_item: &ItemFn,
214
+ cfg: OverrideConfig,
215
+ ) -> syn::Result<Option<Ix>> {
216
+ // Ensure at most one override attribute per function.
217
+ let attr = fn_item
218
+ .attrs
219
+ .iter()
220
+ .filter(|attr| is_override_attr(attr, cfg))
221
+ .at_most_one()
222
+ .map_err(|_| {
223
+ syn::Error::new_spanned(fn_item, format!("duplicate #[{}] on function", cfg.attr_name))
224
+ })?;
225
+
226
+ // Early return if no override attribute is found.
227
+ let Some(attr) = attr else {
228
+ return Ok(None);
229
+ };
230
+
231
+ // Reject arguments — the instruction name is inferred from the function name.
232
+ if !matches!(attr.meta, syn::Meta::Path(_)) {
233
+ return Err(syn::Error::new_spanned(
234
+ attr,
235
+ format!(
236
+ "#[{}] does not accept arguments; the instruction name \
237
+ is inferred from the handler function name",
238
+ cfg.attr_name,
239
+ ),
240
+ ));
241
+ }
242
+
243
+ // Infer the instruction from the handler's function name.
244
+ let fn_ident = &fn_item.sig.ident;
245
+ let ix = Ix::from_ident(fn_ident).ok_or_else(|| {
246
+ syn::Error::new_spanned(
247
+ fn_ident,
248
+ format!("unsupported {} target `{fn_ident}`", cfg.attr_name,),
249
+ )
250
+ })?;
251
+
252
+ Ok(Some(ix))
253
+ }
254
+
255
+ /// Extracts the inner `T` from the first `Context<T>` parameter.
256
+ ///
257
+ /// Returns `(accounts_type, context_type)` — the inner `T` (for default-context
258
+ /// checks) and the full `Context<...>` type as written (to preserve lifetimes in
259
+ /// the generated entrypoint).
260
+ fn infer_context_type(fn_item: &ItemFn) -> syn::Result<(Type, Type)> {
261
+ let Some(syn::FnArg::Typed(first)) = fn_item.sig.inputs.first() else {
262
+ return Err(syn::Error::new_spanned(
263
+ &fn_item.sig,
264
+ "handler must have at least one parameter of type `Context<T>`",
265
+ ));
266
+ };
267
+
268
+ let context_type = (*first.ty).clone();
269
+ let accounts_type = extract_context_inner_type(&first.ty).ok_or_else(|| {
270
+ syn::Error::new_spanned(&first.ty, "expected `Context<T>` as the first parameter")
271
+ })?;
272
+ Ok((accounts_type, context_type))
273
+ }
274
+
275
+ /// Unwraps `Context<T>` or `Context<'a, 'b, 'c, 'info, T>` → `T`.
276
+ ///
277
+ /// Handles both the simple `Context<T>` form and the lifetime-annotated
278
+ /// `Context<'_, '_, '_, 'info, T>` form by finding the last type argument.
279
+ fn extract_context_inner_type(ty: &Type) -> Option<Type> {
280
+ let Type::Path(tp) = ty else { return None };
281
+ let seg = tp.path.segments.last().filter(|s| s.ident == "Context")?;
282
+ let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
283
+ return None;
284
+ };
285
+ args.args.iter().rev().find_map(|arg| match arg {
286
+ syn::GenericArgument::Type(inner) => Some(inner.clone()),
287
+ _ => None,
288
+ })
289
+ }
@@ -0,0 +1,339 @@
1
+ mod common;
2
+
3
+ #[cfg(test)]
4
+ mod tests {
5
+ use super::common::*;
6
+ use anchor_trait::{generate_instructions, Assembly};
7
+ use proc_macro2::TokenStream;
8
+ use quote::{quote, ToTokens};
9
+
10
+ // -----------------------------------------------------------------------
11
+ // Helper
12
+ // -----------------------------------------------------------------------
13
+
14
+ /// Run the full pipeline (collect → generate → assemble) and parse the output.
15
+ fn assemble_ok(
16
+ module: &syn::ItemMod,
17
+ prelude: TokenStream,
18
+ extra_mod_attrs: TokenStream,
19
+ ) -> syn::File {
20
+ let overrides = collect_overrides(module);
21
+ let instructions =
22
+ generate_instructions(&TestCtx, &overrides).expect("generate should succeed");
23
+ let assembly = Assembly {
24
+ prelude,
25
+ overrides,
26
+ instructions,
27
+ extra_mod_attrs,
28
+ source_mod: module.clone(),
29
+ };
30
+ syn::parse2(assembly.assemble()).expect("assembly output should parse")
31
+ }
32
+
33
+ /// Find the program module in a parsed file (skips wrapper modules).
34
+ fn find_program_mod(file: &syn::File) -> Option<(usize, &syn::ItemMod)> {
35
+ file.items.iter().enumerate().find_map(|(i, item)| match item {
36
+ syn::Item::Mod(m) if !m.ident.to_string().starts_with("__") => Some((i, m)),
37
+ _ => None,
38
+ })
39
+ }
40
+
41
+ /// Extract items from inside a module's body.
42
+ fn mod_items(m: &syn::ItemMod) -> &Vec<syn::Item> {
43
+ &m.content.as_ref().expect("module should have a body").1
44
+ }
45
+
46
+ /// A module with only the required lz_receive override.
47
+ fn module_with_required_only() -> syn::ItemMod {
48
+ syn::parse_quote! {
49
+ mod my_program {
50
+ #[test_instruction]
51
+ pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
52
+ Ok(())
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ /// A module with a full override on set_peer + required lz_receive + non-handler items.
59
+ fn module_with_override_and_extras() -> syn::ItemMod {
60
+ syn::parse_quote! {
61
+ mod my_program {
62
+ #[test_instruction]
63
+ pub fn set_peer(ctx: Context<MyCustomAccounts>, params: SetPeerParams) -> Result<()> {
64
+ Ok(())
65
+ }
66
+
67
+ #[test_instruction]
68
+ pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
69
+ Ok(())
70
+ }
71
+
72
+ pub fn helper_function() -> u64 {
73
+ 42
74
+ }
75
+
76
+ struct MyState {
77
+ value: u64,
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ // -----------------------------------------------------------------------
84
+ // Tests — output structure and ordering
85
+ // -----------------------------------------------------------------------
86
+
87
+ #[test]
88
+ fn prelude_emitted_before_module() {
89
+ let module = module_with_required_only();
90
+ let file = assemble_ok(&module, quote! { struct PreludeMarker; }, quote! {});
91
+
92
+ let prelude_pos = file
93
+ .items
94
+ .iter()
95
+ .position(|item| matches!(item, syn::Item::Struct(s) if s.ident == "PreludeMarker"))
96
+ .expect("prelude marker should exist");
97
+ let (mod_pos, _) = find_program_mod(&file).expect("module should exist");
98
+
99
+ assert!(
100
+ prelude_pos < mod_pos,
101
+ "prelude (pos {prelude_pos}) should come before module (pos {mod_pos})"
102
+ );
103
+ }
104
+
105
+ #[test]
106
+ fn wrapper_defs_and_default_impls_outside_module() {
107
+ let module = module_with_override_and_extras();
108
+ let file = assemble_ok(&module, quote! {}, quote! {});
109
+ let (mod_pos, _) = find_program_mod(&file).expect("module should exist");
110
+
111
+ // Wrapper module for the override should be outside and before the program module
112
+ let wrapper_pos = file
113
+ .items
114
+ .iter()
115
+ .position(|item| {
116
+ matches!(item, syn::Item::Mod(m) if m.ident.to_string().contains("__test_instruction__"))
117
+ })
118
+ .expect("wrapper module should exist");
119
+ assert!(
120
+ wrapper_pos < mod_pos,
121
+ "wrapper defs (pos {wrapper_pos}) should come before module (pos {mod_pos})"
122
+ );
123
+
124
+ // Default impls (e.g. StdGetPeer, StdToken) should be outside the module
125
+ // set_peer is fully overridden so no StdSetPeer
126
+ let has_default_impl_outside = file.items[..mod_pos].iter().any(|item| {
127
+ matches!(item, syn::Item::Struct(s) if s.ident == "StdGetPeer" || s.ident == "StdToken")
128
+ });
129
+ assert!(has_default_impl_outside, "default impls should be outside the module");
130
+ }
131
+
132
+ #[test]
133
+ fn extra_mod_attrs_applied_to_module() {
134
+ let module = module_with_required_only();
135
+ let file = assemble_ok(&module, quote! {}, quote! { #[program] });
136
+ let (_, m) = find_program_mod(&file).expect("module should exist");
137
+
138
+ let has_program_attr = m
139
+ .attrs
140
+ .iter()
141
+ .any(|attr| attr.path().segments.last().is_some_and(|seg| seg.ident == "program"));
142
+ assert!(has_program_attr, "module should have #[program] attribute");
143
+ }
144
+
145
+ #[test]
146
+ fn original_mod_attrs_preserved() {
147
+ let module: syn::ItemMod = syn::parse_quote! {
148
+ #[some_custom_attr]
149
+ mod my_program {
150
+ #[test_instruction]
151
+ pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
152
+ Ok(())
153
+ }
154
+ }
155
+ };
156
+ let file = assemble_ok(&module, quote! {}, quote! { #[program] });
157
+ let (_, m) = find_program_mod(&file).expect("module should exist");
158
+
159
+ let has_custom_attr = m.attrs.iter().any(|attr| {
160
+ attr.path().segments.last().is_some_and(|seg| seg.ident == "some_custom_attr")
161
+ });
162
+ assert!(has_custom_attr, "original module attributes should be preserved");
163
+ }
164
+
165
+ #[test]
166
+ fn filtered_items_inside_module() {
167
+ let module = module_with_override_and_extras();
168
+ let file = assemble_ok(&module, quote! {}, quote! {});
169
+ let (_, m) = find_program_mod(&file).expect("module should exist");
170
+ let items = mod_items(m);
171
+
172
+ // Override handler should NOT be a function definition inside the module
173
+ // (it was relocated to wrapper). Note: the name still appears in dispatch
174
+ // calls inside entrypoints, so we check for function definitions specifically.
175
+ let has_override_fn = items.iter().any(|item| {
176
+ matches!(item, syn::Item::Fn(f) if f.sig.ident == "set_peer"
177
+ && f.block.to_token_stream().to_string().contains("MyCustomAccounts"))
178
+ });
179
+ assert!(
180
+ !has_override_fn,
181
+ "overridden handler with custom body should be filtered out of module body"
182
+ );
183
+
184
+ // Non-handler items should be preserved inside the module
185
+ let has_helper = items
186
+ .iter()
187
+ .any(|item| matches!(item, syn::Item::Fn(f) if f.sig.ident == "helper_function"));
188
+ assert!(has_helper, "helper_function should be inside module");
189
+
190
+ let has_struct = items
191
+ .iter()
192
+ .any(|item| matches!(item, syn::Item::Struct(s) if s.ident == "MyState"));
193
+ assert!(has_struct, "MyState struct should be inside module");
194
+ }
195
+
196
+ #[test]
197
+ fn entrypoints_inside_module() {
198
+ let module = module_with_required_only();
199
+ let file = assemble_ok(&module, quote! {}, quote! {});
200
+ let (_, m) = find_program_mod(&file).expect("module should exist");
201
+ let items = mod_items(m);
202
+
203
+ let fns: Vec<_> = items
204
+ .iter()
205
+ .filter_map(|item| match item {
206
+ syn::Item::Fn(f) => Some(f.sig.ident.to_string()),
207
+ _ => None,
208
+ })
209
+ .collect();
210
+
211
+ assert!(
212
+ fns.contains(&"set_peer".to_string()),
213
+ "set_peer entrypoint should be inside module"
214
+ );
215
+ assert!(
216
+ fns.contains(&"get_peer".to_string()),
217
+ "get_peer entrypoint should be inside module"
218
+ );
219
+ assert!(
220
+ fns.contains(&"lz_receive".to_string()),
221
+ "lz_receive entrypoint should be inside module"
222
+ );
223
+ assert!(fns.contains(&"token".to_string()), "token entrypoint should be inside module");
224
+ }
225
+
226
+ #[test]
227
+ fn wrapper_defs_contain_override_handlers() {
228
+ let module = module_with_override_and_extras();
229
+ let file = assemble_ok(&module, quote! {}, quote! {});
230
+
231
+ // Find the wrapper module for set_peer in the assembled output
232
+ let wrapper = file
233
+ .items
234
+ .iter()
235
+ .find_map(|item| match item {
236
+ syn::Item::Mod(m)
237
+ if m.ident.to_string().contains("__test_instruction__set_peer") =>
238
+ {
239
+ Some(m)
240
+ },
241
+ _ => None,
242
+ })
243
+ .expect("wrapper module for set_peer should exist in assembled output");
244
+
245
+ // The relocated handler should be inside the wrapper
246
+ let wrapper_items = mod_items(wrapper);
247
+ let has_handler = wrapper_items
248
+ .iter()
249
+ .any(|item| matches!(item, syn::Item::Fn(f) if f.sig.ident == "set_peer"));
250
+ assert!(has_handler, "wrapper should contain the relocated override handler");
251
+ }
252
+
253
+ #[test]
254
+ fn module_visibility_preserved() {
255
+ let module: syn::ItemMod = syn::parse_quote! {
256
+ pub mod my_program {
257
+ #[test_instruction]
258
+ pub fn lz_receive(ctx: Context<MyReceive>, params: LzReceiveParams) -> Result<()> {
259
+ Ok(())
260
+ }
261
+ }
262
+ };
263
+ let file = assemble_ok(&module, quote! {}, quote! {});
264
+ let (_, m) = find_program_mod(&file).expect("module should exist");
265
+
266
+ assert!(
267
+ matches!(m.vis, syn::Visibility::Public(_)),
268
+ "pub visibility should be preserved on module"
269
+ );
270
+ }
271
+
272
+ #[test]
273
+ fn use_super_star_injected_into_module() {
274
+ let module = module_with_required_only();
275
+ let file = assemble_ok(&module, quote! {}, quote! {});
276
+ let (_, m) = find_program_mod(&file).expect("module should exist");
277
+ let items = mod_items(m);
278
+
279
+ let has_use_super = items.iter().any(|item| {
280
+ matches!(item, syn::Item::Use(u) if u.to_token_stream().to_string().contains("super :: *"))
281
+ });
282
+ assert!(has_use_super, "module should contain `use super::*`");
283
+ }
284
+
285
+ #[test]
286
+ fn full_assembly_integration() {
287
+ let module = module_with_override_and_extras();
288
+ let file = assemble_ok(&module, quote! { struct PreludeMarker; }, quote! { #[program] });
289
+
290
+ // Verify complete ordering: prelude → wrapper → default impls → module
291
+ let prelude_pos = file
292
+ .items
293
+ .iter()
294
+ .position(|item| matches!(item, syn::Item::Struct(s) if s.ident == "PreludeMarker"))
295
+ .expect("prelude should exist");
296
+
297
+ let wrapper_pos = file
298
+ .items
299
+ .iter()
300
+ .position(|item| {
301
+ matches!(item, syn::Item::Mod(m) if m.ident.to_string().contains("__test_instruction__"))
302
+ })
303
+ .expect("wrapper should exist");
304
+
305
+ let context_pos = file
306
+ .items
307
+ .iter()
308
+ .position(|item| {
309
+ matches!(item, syn::Item::Struct(s) if s.ident == "StdGetPeer" || s.ident == "StdToken")
310
+ })
311
+ .expect("default impl struct should exist");
312
+
313
+ let (mod_pos, m) = find_program_mod(&file).expect("module should exist");
314
+
315
+ // Ordering: prelude < wrapper < default impls < module
316
+ assert!(prelude_pos < wrapper_pos, "prelude before wrapper");
317
+ assert!(wrapper_pos < context_pos, "wrapper before default impls");
318
+ assert!(context_pos < mod_pos, "default impls before module");
319
+
320
+ // Module has #[program]
321
+ let has_program = m
322
+ .attrs
323
+ .iter()
324
+ .any(|attr| attr.path().segments.last().is_some_and(|seg| seg.ident == "program"));
325
+ assert!(has_program, "module should have #[program]");
326
+
327
+ // Entrypoints inside module
328
+ let items = mod_items(m);
329
+ let fn_names: Vec<_> = items
330
+ .iter()
331
+ .filter_map(|item| match item {
332
+ syn::Item::Fn(f) => Some(f.sig.ident.to_string()),
333
+ _ => None,
334
+ })
335
+ .collect();
336
+ assert!(fn_names.contains(&"set_peer".to_string()), "set_peer inside module");
337
+ assert!(fn_names.contains(&"token".to_string()), "token inside module");
338
+ }
339
+ }