1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.

// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.

#![deny(unused_crate_dependencies)]
#![deny(missing_docs)]
#![deny(clippy::dbg_macro)]

//! Generative part of `tracing-gum`. See `tracing-gum` for usage documentation.

use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{parse2, parse_quote, punctuated::Punctuated, Result, Token};

mod types;

use self::types::*;

#[cfg(test)]
mod tests;

/// Print an error message.
#[proc_macro]
pub fn error(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	gum(item, Level::Error)
}

/// Print a warning level message.
#[proc_macro]
pub fn warn(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	gum(item, Level::Warn)
}

/// Print a warning or debug level message depending on their frequency
#[proc_macro]
pub fn warn_if_frequent(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	let ArgsIfFrequent { freq, max_rate, rest } = parse2(item.into()).unwrap();

	let freq_expr = freq.expr;
	let max_rate_expr = max_rate.expr;
	let debug: proc_macro2::TokenStream = gum(rest.clone().into(), Level::Debug).into();
	let warn: proc_macro2::TokenStream = gum(rest.into(), Level::Warn).into();

	let stream = quote! {
		if #freq_expr .is_frequent(#max_rate_expr) {
			#warn
		} else {
			#debug
		}
	};

	stream.into()
}

/// Print a info level message.
#[proc_macro]
pub fn info(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	gum(item, Level::Info)
}

/// Print a debug level message.
#[proc_macro]
pub fn debug(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	gum(item, Level::Debug)
}

/// Print a trace level message.
#[proc_macro]
pub fn trace(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
	gum(item, Level::Trace)
}

/// One-size-fits all internal implementation that produces the actual code.
pub(crate) fn gum(item: proc_macro::TokenStream, level: Level) -> proc_macro::TokenStream {
	let item: TokenStream = item.into();

	let res = expander::Expander::new("gum")
		.add_comment("Generated overseer code by `gum::warn!(..)`".to_owned())
		// `dry=true` until rust-analyzer can selectively disable features so it's
		// not all red squiggles. Originally: `!cfg!(feature = "expand")`
		// ISSUE: <https://github.com/rust-analyzer/rust-analyzer/issues/11777>
		.dry(true)
		.verbose(false)
		.fmt(expander::Edition::_2021)
		.maybe_write_to_out_dir(impl_gum2(item, level))
		.expect("Expander does not fail due to IO in OUT_DIR. qed");

	res.unwrap_or_else(|err| err.to_compile_error()).into()
}

/// Does the actual parsing and token generation based on `proc_macro2` types.
///
/// Required for unit tests.
pub(crate) fn impl_gum2(orig: TokenStream, level: Level) -> Result<TokenStream> {
	let args: Args = parse2(orig)?;

	let krate = support_crate();
	let span = Span::call_site();

	let Args { target, comma, mut values, fmt } = args;

	// find a value or alias called `candidate_hash`.
	let maybe_candidate_hash = values.iter_mut().find(|value| value.as_ident() == "candidate_hash");

	if let Some(kv) = maybe_candidate_hash {
		let (ident, rhs_expr, replace_with) = match kv {
			Value::Alias(alias) => {
				let ValueWithAliasIdent { alias, marker, expr, .. } = alias.clone();
				(
					alias.clone(),
					expr.to_token_stream(),
					Some(Value::Value(ValueWithFormatMarker {
						marker,
						ident: alias,
						dot: None,
						inner: Punctuated::new(),
					})),
				)
			},
			Value::Value(value) => (value.ident.clone(), value.ident.to_token_stream(), None),
		};

		// we generate a local value with the same alias name
		// so replace the expr with just a value
		if let Some(replace_with) = replace_with {
			let _old = std::mem::replace(kv, replace_with);
		};

		// Inject the addition `traceID = % trace_id` identifier
		// while maintaining trailing comma semantics.
		let had_trailing_comma = values.trailing_punct();
		if !had_trailing_comma {
			values.push_punct(Token![,](span));
		}

		values.push_value(parse_quote! {
			traceID = % trace_id
		});
		if had_trailing_comma {
			values.push_punct(Token![,](span));
		}

		Ok(quote! {
			if #krate :: enabled!(#target #comma #level) {
				use ::std::ops::Deref;

				// create a scoped let binding of something that `deref`s to
				// `Hash`.
				let value = #rhs_expr;
				let value = &value;
				let value: & #krate:: Hash = value.deref();
				// Do the `deref` to `Hash` and convert to a `TraceIdentifier`.
				let #ident: #krate:: Hash = * value;
				let trace_id = #krate:: hash_to_trace_identifier ( #ident );
				#krate :: event!(
					#target #comma #level, #values #fmt
				)
			}
		})
	} else {
		Ok(quote! {
				#krate :: event!(
					#target #comma #level, #values #fmt
				)
		})
	}
}

/// Extract the support crate path.
fn support_crate() -> TokenStream {
	let support_crate_name = if cfg!(test) {
		quote! {crate}
	} else {
		use proc_macro_crate::{crate_name, FoundCrate};
		let crate_name = crate_name("tracing-gum")
			.expect("Support crate `tracing-gum` is present in `Cargo.toml`. qed");
		match crate_name {
			FoundCrate::Itself => quote! {crate},
			FoundCrate::Name(name) => Ident::new(&name, Span::call_site()).to_token_stream(),
		}
	};
	support_crate_name
}