@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.
- package/Cargo.toml +18 -0
- package/examples/shapes/Anchor.toml +19 -0
- package/examples/shapes/macros/Cargo.toml +17 -0
- package/examples/shapes/macros/src/instructions/compute_area.rs +50 -0
- package/examples/shapes/macros/src/instructions/compute_perimeter.rs +50 -0
- package/examples/shapes/macros/src/instructions/get_dimension_count.rs +44 -0
- package/examples/shapes/macros/src/instructions/get_name.rs +18 -0
- package/examples/shapes/macros/src/instructions/initialize.rs +18 -0
- package/examples/shapes/macros/src/instructions/mod.rs +26 -0
- package/examples/shapes/macros/src/lib.rs +50 -0
- package/examples/shapes/macros/src/shapes.rs +56 -0
- package/examples/shapes/programs/ellipse/Cargo.toml +30 -0
- package/examples/shapes/programs/ellipse/src/lib.rs +88 -0
- package/examples/shapes/programs/ellipse/src/state.rs +9 -0
- package/examples/shapes/programs/triangle/Cargo.toml +30 -0
- package/examples/shapes/programs/triangle/src/lib.rs +83 -0
- package/examples/shapes/programs/triangle/src/state.rs +9 -0
- package/package.json +36 -0
- package/rust-toolchain.toml +3 -0
- package/rustfmt.toml +23 -0
- package/src/assembly.rs +63 -0
- package/src/generation.rs +226 -0
- package/src/lib.rs +126 -0
- package/src/overrides.rs +289 -0
- package/tests/assembly.rs +339 -0
- package/tests/common/mod.rs +206 -0
- package/tests/declare_instructions.rs +206 -0
- package/tests/generation.rs +358 -0
- package/tests/override.rs +414 -0
- package/tests/snapshots/snapshots__tests__snapshot_all_defaults.snap +35 -0
- package/tests/snapshots/snapshots__tests__snapshot_full_override.snap +47 -0
- package/tests/snapshots/snapshots__tests__snapshot_handler_override.snap +42 -0
- package/tests/snapshots/snapshots__tests__snapshot_mixed_overrides.snap +57 -0
- package/tests/snapshots.rs +139 -0
package/src/overrides.rs
ADDED
|
@@ -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
|
+
}
|