cu29_log_derive/
lib.rs

1extern crate proc_macro;
2
3use cu29_intern_strs::intern_string;
4use cu29_log::CuLogLevel;
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::parse::Parser;
8#[cfg(debug_assertions)]
9use syn::spanned::Spanned;
10use syn::Token;
11use syn::{Expr, ExprAssign, ExprLit, Lit};
12
13/// Create reference of unused_variables to avoid warnings
14/// ex: let _ = &tmp;
15#[allow(unused)]
16fn reference_unused_variables(input: TokenStream) -> TokenStream {
17    // Attempt to parse the expressions to "use" them.
18    // This ensures variables passed to the macro are considered used by the compiler.
19    let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
20    if let Ok(exprs) = parser.parse(input.clone()) {
21        let mut var_usages = Vec::new();
22        // Skip the first expression, which is assumed to be the format string literal.
23        // We only care about "using" the subsequent variable arguments.
24        for expr in exprs.iter().skip(1) {
25            match expr {
26                // If the argument is an assignment (e.g., `foo = bar`),
27                // we need to ensure `bar` (the right-hand side) is "used".
28                syn::Expr::Assign(assign_expr) => {
29                    let value_expr = &assign_expr.right;
30                    var_usages.push(quote::quote! { let _ = &#value_expr; });
31                }
32                // Otherwise, for any other expression, ensure it's "used".
33                _ => {
34                    var_usages.push(quote::quote! { let _ = &#expr; });
35                }
36            }
37        }
38        // Return a block that contains these dummy "usages".
39        // If only a format string was passed, var_usages will be empty,
40        // resulting in an empty block `{}`, which is fine.
41        return quote::quote! { { #(#var_usages;)* } }.into();
42    }
43
44    // Fallback: if parsing fails for some reason, return an empty TokenStream.
45    // This might still lead to warnings if parsing failed but is better than panicking.
46    proc_macro::TokenStream::new()
47}
48
49/// Create a log entry at the specified log level.
50///
51/// This is the internal macro implementation used by all the logging macros.
52/// Users should use the public-facing macros: `debug!`, `info!`, `warning!`, `error!`, or `critical!`.
53#[allow(unused)]
54fn create_log_entry(input: TokenStream, level: CuLogLevel) -> TokenStream {
55    let parser = syn::punctuated::Punctuated::<Expr, Token![,]>::parse_terminated;
56    let exprs = parser.parse(input).expect("Failed to parse input");
57
58    let mut exprs_iter = exprs.iter();
59
60    let msg_expr = exprs_iter.next().expect("Expected at least one expression");
61    let (index, _msg) = if let Expr::Lit(ExprLit {
62        lit: Lit::Str(msg), ..
63    }) = msg_expr
64    {
65        let msg = msg.value();
66        let index = intern_string(&msg).expect("Failed to insert log string.");
67        (index, msg)
68    } else {
69        panic!("The first parameter of the argument needs to be a string literal.");
70    };
71    let level_str = match level {
72        CuLogLevel::Debug => quote! { Debug },
73        CuLogLevel::Info => quote! { Info },
74        CuLogLevel::Warning => quote! { Warning },
75        CuLogLevel::Error => quote! { Error },
76        CuLogLevel::Critical => quote! { Critical },
77    };
78    let prefix = quote! {
79        let mut log_entry = CuLogEntry::new(#index, CuLogLevel::#level_str);
80    };
81
82    let mut unnamed_params = vec![];
83    let mut named_params = vec![];
84
85    for expr in exprs_iter {
86        if let Expr::Assign(ExprAssign { left, right, .. }) = expr {
87            named_params.push((left, right));
88        } else {
89            unnamed_params.push(expr);
90        }
91    }
92
93    let unnamed_prints = unnamed_params.iter().map(|value| {
94        quote! {
95            let param = to_value(#value).expect("Failed to convert a parameter to a Value");
96            log_entry.add_param(ANONYMOUS, param);
97        }
98    });
99
100    let named_prints = named_params.iter().map(|(name, value)| {
101        let index = intern_string(quote!(#name).to_string().as_str())
102            .expect("Failed to insert log string.");
103        quote! {
104            let param = to_value(#value).expect("Failed to convert a parameter to a Value");
105            log_entry.add_param(#index, param);
106        }
107    });
108
109    #[cfg(not(debug_assertions))]
110    let log_stmt = quote! {
111        let r = log(&mut log_entry);
112    };
113
114    #[cfg(debug_assertions)]
115    let log_stmt = {
116        let keys: Vec<_> = named_params
117            .iter()
118            .map(|(name, _)| {
119                let name_str = quote!(#name).to_string(); // Convert the expression to a token stream, then to a string
120                let lit_str = syn::LitStr::new(&name_str, name.span()); // Create a string literal with the original span
121                quote!(#lit_str)
122            })
123            .collect();
124        quote! {
125            let r = log_debug_mode(&mut log_entry, #_msg, &[#(#keys),*]);
126        }
127    };
128
129    let postfix = quote! {
130        #log_stmt
131        if let Err(e) = r {
132            eprintln!("Warning: Failed to log: {}", e);
133            let backtrace = std::backtrace::Backtrace::capture();
134            eprintln!("{:?}", backtrace);
135        }
136    };
137
138    let expanded = quote! {
139        {
140            #prefix
141            #(#unnamed_prints)*
142            #(#named_prints)*
143            #postfix
144        }
145    };
146
147    expanded.into()
148}
149
150/// This macro is used to log a debug message with parameters.
151/// The first parameter is a string literal that represents the message to be logged.
152/// Only `{}` is supported as a placeholder for parameters.
153/// The rest of the parameters are the values to be logged.
154/// The parameters can be named or unnamed.
155/// Named parameters are specified as `name = value`.
156/// Unnamed parameters are specified as `value`.
157/// # Example
158/// ```ignore
159/// use cu29_log_derive::debug;
160/// let a = 1;
161/// let b = 2;
162/// debug!("a = {}, b = {}", my_value = a, b); // named and unnamed parameters
163/// ```
164///
165/// You can retrieve this data using the log_reader generated with your project and giving it the
166/// unified .copper log file and the string index file generated at compile time.
167///
168/// Note: In debug mode, the log will also be printed to the console. (ie slooow).
169/// In release mode, the log will be only be written to the unified logger.
170///
171/// This macro will be compiled out if the max log level is set to a level higher than Debug.
172#[cfg(feature = "log-level-debug")]
173#[proc_macro]
174pub fn debug(input: TokenStream) -> TokenStream {
175    create_log_entry(input, CuLogLevel::Debug)
176}
177
178/// This macro is used to log an info message with parameters.
179/// The first parameter is a string literal that represents the message to be logged.
180/// Only `{}` is supported as a placeholder for parameters.
181/// The rest of the parameters are the values to be logged.
182///
183/// This macro will be compiled out if the max log level is set to a level higher than Info.
184#[cfg(any(feature = "log-level-debug", feature = "log-level-info",))]
185#[proc_macro]
186pub fn info(input: TokenStream) -> TokenStream {
187    create_log_entry(input, CuLogLevel::Info)
188}
189
190/// This macro is used to log a warning message with parameters.
191/// The first parameter is a string literal that represents the message to be logged.
192/// Only `{}` is supported as a placeholder for parameters.
193/// The rest of the parameters are the values to be logged.
194///
195/// This macro will be compiled out if the max log level is set to a level higher than Warning.
196#[cfg(any(
197    feature = "log-level-debug",
198    feature = "log-level-info",
199    feature = "log-level-warning",
200))]
201#[proc_macro]
202pub fn warning(input: TokenStream) -> TokenStream {
203    create_log_entry(input, CuLogLevel::Warning)
204}
205
206/// This macro is used to log an error message with parameters.
207/// The first parameter is a string literal that represents the message to be logged.
208/// Only `{}` is supported as a placeholder for parameters.
209/// The rest of the parameters are the values to be logged.
210///
211/// This macro will be compiled out if the max log level is set to a level higher than Error.
212#[cfg(any(
213    feature = "log-level-debug",
214    feature = "log-level-info",
215    feature = "log-level-warning",
216    feature = "log-level-error",
217))]
218#[proc_macro]
219pub fn error(input: TokenStream) -> TokenStream {
220    create_log_entry(input, CuLogLevel::Error)
221}
222
223/// This macro is used to log a critical message with parameters.
224/// The first parameter is a string literal that represents the message to be logged.
225/// Only `{}` is supported as a placeholder for parameters.
226/// The rest of the parameters are the values to be logged.
227///
228/// This macro is always compiled in, regardless of the max log level setting.
229#[cfg(any(
230    feature = "log-level-debug",
231    feature = "log-level-info",
232    feature = "log-level-warning",
233    feature = "log-level-error",
234    feature = "log-level-critical",
235))]
236#[proc_macro]
237pub fn critical(input: TokenStream) -> TokenStream {
238    create_log_entry(input, CuLogLevel::Critical)
239}
240
241// Provide empty implementations for macros that are compiled out
242#[cfg(not(any(feature = "log-level-debug",)))]
243#[proc_macro]
244pub fn debug(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
245    reference_unused_variables(input)
246}
247
248#[cfg(not(any(feature = "log-level-debug", feature = "log-level-info",)))]
249#[proc_macro]
250pub fn info(input: TokenStream) -> TokenStream {
251    reference_unused_variables(input)
252}
253
254#[cfg(not(any(
255    feature = "log-level-debug",
256    feature = "log-level-info",
257    feature = "log-level-warning",
258)))]
259#[proc_macro]
260pub fn warning(input: TokenStream) -> TokenStream {
261    reference_unused_variables(input)
262}
263
264#[cfg(not(any(
265    feature = "log-level-debug",
266    feature = "log-level-info",
267    feature = "log-level-warning",
268    feature = "log-level-error",
269)))]
270#[proc_macro]
271pub fn error(input: TokenStream) -> TokenStream {
272    reference_unused_variables(input)
273}
274
275#[cfg(not(any(
276    feature = "log-level-debug",
277    feature = "log-level-info",
278    feature = "log-level-warning",
279    feature = "log-level-error",
280    feature = "log-level-critical",
281)))]
282#[proc_macro]
283pub fn critical(input: TokenStream) -> TokenStream {
284    reference_unused_variables(input)
285}
286
287/// Interns a string
288/// For example:
289///
290/// let string_number: u32 = intern!("my string");
291///
292/// will store "my string" in the interned string db at compile time and return the index of the string.
293#[proc_macro]
294pub fn intern(input: TokenStream) -> TokenStream {
295    let expr = syn::parse::<Expr>(input).expect("Failed to parse input as expression");
296    let (index, _msg) = if let Expr::Lit(ExprLit {
297        lit: Lit::Str(msg), ..
298    }) = expr
299    {
300        let msg = msg.value();
301        let index = intern_string(&msg).expect("Failed to insert log string.");
302        (index, msg)
303    } else {
304        panic!("The first parameter of the argument needs to be a string literal.");
305    };
306    quote! { #index }.into()
307}