Skip to main content

lenso_contract_codegen/
module_projection.rs

1//! Keep local contract macros hygienic when several projections share a crate.
2use 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
12/// Generate a Rust projection loaded as a normal module through `#[path]`.
13/// `module` is the exact public path from the consuming crate's root.
14pub 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    // Token spans let us change paths and names without changing string literals
76    // or the generated contract's types, schemas, descriptors and wire metadata.
77    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
105/// Write an exact, reproducible in-package Rust projection.
106pub 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
118/// Check an in-package projection without editing generated files.
119pub 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}