lenso_contract_codegen/
module_projection.rs1use crate::{CodegenError, GeneratedProjection, ProjectionLanguage, generate_projection};
3use proc_macro2::{TokenStream, TokenTree};
4use std::{fmt::Write as _, path::Path};
5
6fn invalid(detail: impl Into<String>) -> CodegenError {
7 CodegenError::InvalidDescriptor {
8 detail: detail.into(),
9 }
10}
11
12pub fn generate_module_projection(
15 path: &Path,
16 language: ProjectionLanguage,
17 module: &str,
18) -> Result<GeneratedProjection, CodegenError> {
19 if !matches!(
20 language,
21 ProjectionLanguage::Rust | ProjectionLanguage::RustRuntime
22 ) {
23 return Err(invalid("module projections require rust or rust-runtime"));
24 }
25 let parsed: syn::Path =
26 syn::parse_str(module).map_err(|_| invalid("invalid Rust contract module path"))?;
27 if parsed.leading_colon.is_some()
28 || parsed.segments.iter().any(|s| {
29 !matches!(s.arguments, syn::PathArguments::None)
30 || matches!(
31 s.ident.to_string().as_str(),
32 "crate" | "self" | "super" | "Self"
33 )
34 })
35 {
36 return Err(invalid(
37 "contract module must be a relative named path from the crate root",
38 ));
39 }
40 let module = parsed
41 .segments
42 .iter()
43 .map(|s| s.ident.to_string())
44 .collect::<Vec<_>>()
45 .join("::");
46 let mut prefix = String::new();
47 for segment in &parsed.segments {
48 let name = segment.ident.to_string();
49 let name = name.strip_prefix("r#").unwrap_or(&name);
50 write!(prefix, "_{}_{}", name.len(), name).expect("String writes do not fail");
51 }
52 let mut projection = generate_projection(path, language)?;
53 let file = syn::parse_file(&projection.source)
54 .map_err(|e| invalid(format!("invalid generated Rust projection: {e}")))?;
55 let mut replacements = Vec::new();
56 let mut aliases = String::new();
57 for item in file.items {
58 if let syn::Item::Macro(item) = item {
59 if !item.mac.path.is_ident("macro_rules") {
60 continue;
61 }
62 let Some(name) = item.ident else {
63 continue;
64 };
65 let exported = format!("__lenso_contract{prefix}_{name}");
66 replacements.push((name.span().byte_range(), exported.clone()));
67 write!(
68 aliases,
69 "#[doc(hidden)]\npub use crate::{exported} as {name};\n"
70 )
71 .expect("String writes do not fail");
72 namespace(item.mac.tokens, &module, &mut replacements);
73 }
74 }
75 replacements.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
78 for (range, replacement) in replacements {
79 projection.source.replace_range(range, &replacement);
80 }
81 projection.source.push_str(&aliases);
82 Ok(projection)
83}
84
85fn namespace(
86 tokens: TokenStream,
87 module: &str,
88 replacements: &mut Vec<(std::ops::Range<usize>, String)>,
89) {
90 let tokens = tokens.into_iter().collect::<Vec<_>>();
91 for (index, token) in tokens.iter().enumerate() {
92 if let TokenTree::Group(group) = token {
93 namespace(group.stream(), module, replacements);
94 }
95 if let TokenTree::Ident(name) = token
96 && name == "crate"
97 && index > 0
98 && matches!(&tokens[index - 1], TokenTree::Punct(p) if p.as_char() == '$')
99 {
100 replacements.push((name.span().byte_range(), format!("crate::{module}")));
101 }
102 }
103}
104
105pub fn write_module_projection(
107 descriptor: &Path,
108 language: ProjectionLanguage,
109 module: &str,
110 output: &Path,
111) -> Result<(), CodegenError> {
112 crate::write_artifact(
113 output,
114 &generate_module_projection(descriptor, language, module)?.source,
115 )
116}
117
118pub fn check_module_projection(
120 descriptor: &Path,
121 language: ProjectionLanguage,
122 module: &str,
123 output: &Path,
124) -> Result<(), CodegenError> {
125 crate::check_artifact(
126 output,
127 &generate_module_projection(descriptor, language, module)?.source,
128 )
129}