1extern crate proc_macro;
5
6use proc_macro::TokenStream;
7use proc_macro2::Punct;
8use quote::{ToTokens, TokenStreamExt, quote};
9use syn::parse::{Parse, Parser};
10use syn::punctuated::Punctuated;
11use syn::token::Comma;
12use syn::{Expr, ItemFn, Meta, MetaList, Token, parse_quote, parse2};
13
14struct Fields(MetaList);
15impl From<MetaList> for Fields {
16 fn from(value: MetaList) -> Self {
17 Fields(value)
18 }
19}
20
21impl Fields {
22 fn create_with_servo_profiling() -> Self {
23 Fields(parse_quote! { fields(servo_profiling = true) })
24 }
25
26 fn inject_servo_profiling(&mut self) -> syn::Result<()> {
27 let metalist = std::mem::replace(&mut self.0, parse_quote! {field()});
28
29 let arguments: Punctuated<Meta, Comma> =
30 Punctuated::parse_terminated.parse2(metalist.tokens)?;
31
32 let servo_profile_given = arguments
33 .iter()
34 .any(|arg| arg.path().is_ident("servo_profiling"));
35
36 let metalist = if servo_profile_given {
37 parse_quote! {
38 fields(#arguments)
39 }
40 } else {
41 parse_quote! {
42 fields(servo_profiling=true, #arguments)
43 }
44 };
45
46 let _ = std::mem::replace(&mut self.0, metalist);
47
48 Ok(())
49 }
50}
51
52impl ToTokens for Fields {
53 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
54 let items = &self.0;
55 tokens.append_all(quote! { #items });
56 }
57}
58enum Directive {
59 Passthrough(Meta),
60 Level(Expr),
61 Fields(Fields),
62}
63
64impl From<Fields> for Directive {
65 fn from(value: Fields) -> Self {
66 Directive::Fields(value)
67 }
68}
69
70impl Directive {
71 fn is_level(&self) -> bool {
72 matches!(self, Directive::Level(..))
73 }
74
75 fn fields_mut(&mut self) -> Option<&mut Fields> {
76 match self {
77 Directive::Fields(fields) => Some(fields),
78 _ => None,
79 }
80 }
81}
82
83impl ToTokens for Directive {
84 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
85 match self {
86 Directive::Passthrough(meta) => tokens.append_all(quote! { #meta }),
87 Directive::Level(level) => tokens.append_all(quote! { level = #level }),
88 Directive::Fields(fields) => tokens.append_all(quote! { #fields }),
89 };
90 }
91}
92
93impl ToTokens for InstrumentConfiguration {
94 fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
95 tokens.append_terminated(&self.0, Punct::new(',', proc_macro2::Spacing::Joint));
96 }
97}
98
99struct InstrumentConfiguration(Vec<Directive>);
100
101impl InstrumentConfiguration {
102 fn inject_servo_profiling(&mut self) -> syn::Result<()> {
103 let fields = self.0.iter_mut().find_map(Directive::fields_mut);
104 match fields {
105 None => {
106 self.0
107 .push(Directive::from(Fields::create_with_servo_profiling()));
108 Ok(())
109 },
110 Some(fields) => fields.inject_servo_profiling(),
111 }
112 }
113
114 fn inject_level(&mut self) {
115 if self.0.iter().any(|a| a.is_level()) {
116 return;
117 }
118 self.0.push(Directive::Level(parse_quote! { "trace" }));
119 }
120}
121
122impl Parse for InstrumentConfiguration {
123 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
124 let args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
125 let mut components = vec![];
126
127 for arg in args {
128 match arg {
129 Meta::List(meta_list) if meta_list.path.is_ident("fields") => {
130 components.push(Directive::Fields(meta_list.into()));
131 },
132 Meta::NameValue(meta_name_value) if meta_name_value.path.is_ident("level") => {
133 components.push(Directive::Level(meta_name_value.value));
134 },
135 _ => {
136 components.push(Directive::Passthrough(arg));
137 },
138 }
139 }
140 Ok(InstrumentConfiguration(components))
141 }
142}
143
144fn instrument_internal(
145 attr: proc_macro2::TokenStream,
146 item: proc_macro2::TokenStream,
147) -> syn::Result<proc_macro2::TokenStream> {
148 let mut configuration: InstrumentConfiguration = parse2(attr)?;
150 let input_fn: ItemFn = parse2(item)?;
151
152 configuration.inject_servo_profiling()?;
153 configuration.inject_level();
154
155 let output = quote! {
156 #[cfg_attr(
157 feature = "tracing",
158 tracing::instrument(
159 #configuration
160 )
161 )]
162 #input_fn
163 };
164
165 Ok(output)
166}
167
168#[proc_macro_attribute]
169pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream {
196 match instrument_internal(attr.into(), item.into()) {
197 Ok(stream) => stream.into(),
198 Err(err) => err.to_compile_error().into(),
199 }
200}
201
202#[cfg(test)]
203mod test {
204 use proc_macro2::TokenStream;
205 use quote::{ToTokens, quote};
206 use syn::{Attribute, ItemFn};
207
208 use crate::instrument_internal;
209
210 fn extract_instrument_attribute(item_fn: &mut ItemFn) -> TokenStream {
211 let attr: &Attribute = item_fn
212 .attrs
213 .iter()
214 .find(|attr| {
215 let p = attr.path().to_token_stream().to_string();
218 p == "servo_tracing :: instrument"
219 })
220 .expect("Attribute `servo_tracing::instrument` not found");
221
222 let attr_args = attr
224 .parse_args::<TokenStream>()
225 .expect("Failed to parse attribute args");
226
227 item_fn.attrs.retain(|attr| {
229 attr.path().to_token_stream().to_string() != "servo_tracing :: instrument"
230 });
231
232 attr_args
233 }
234
235 fn evaluate(function: TokenStream, test_case: TokenStream, expected: TokenStream) {
241 let test_case = quote! {
242 #test_case
243 #function
244 };
245 let expected = quote! {
246 #expected
247 #function
248 };
249 let function_str = function.to_string();
250 let function_str = syn::parse_file(&function_str).expect("function to have valid syntax");
251 let function_str = prettyplease::unparse(&function_str);
252
253 let mut item_fn: ItemFn =
254 syn::parse2(test_case).expect("Failed to parse input as function");
255
256 let attr_args = extract_instrument_attribute(&mut item_fn);
257 let item_fn = item_fn.to_token_stream();
258
259 let generated = instrument_internal(attr_args, item_fn).expect("Generation to not fail.");
260
261 let generated = syn::parse_file(generated.to_string().as_str())
262 .expect("to have generated a valid function");
263 let generated = prettyplease::unparse(&generated);
264 let expected = syn::parse_file(expected.to_string().as_str())
265 .expect("to have been given a valid expected function");
266 let expected = prettyplease::unparse(&expected);
267
268 eprintln!(
269 "Generated:---------:\n{}--------\nExpected:----------\n{}",
270 &generated, &expected
271 );
272 assert_eq!(generated, expected);
273 assert!(
274 generated.contains(&function_str),
275 "Expected generated code: {generated} to contain the function code: {function_str}"
276 );
277 }
278
279 fn function1() -> TokenStream {
280 quote! {
281 pub fn start(
282 state: (),
283 layout_factory: (),
284 random_pipeline_closure_probability: (),
285 random_pipeline_closure_seed: (),
286 hard_fail: (),
287 canvas_create_sender: (),
288 canvas_ipc_sender: (),
289 ) {
290 }
291 }
292 }
293
294 fn function2() -> TokenStream {
295 quote! {
296 fn layout(
297 mut self,
298 layout_context: &LayoutContext,
299 positioning_context: &mut PositioningContext,
300 containing_block_for_children: &ContainingBlock,
301 containing_block_for_table: &ContainingBlock,
302 depends_on_block_constraints: bool,
303 ) {
304 }
305 }
306 }
307
308 #[test]
309 fn passing_servo_profiling_and_level_and_aux() {
310 let function = function1();
311 let expected = quote! {
312 #[cfg_attr(
313 feature = "tracing",
314 tracing::instrument(skip(state, layout_factory), fields(servo_profiling = true), level = "trace",)
315 )]
316 };
317
318 let test_case = quote! {
319 #[servo_tracing::instrument(skip(state, layout_factory),fields(servo_profiling = true),level = "trace",)]
320 };
321
322 evaluate(function, test_case, expected);
323 }
324
325 #[test]
326 fn passing_servo_profiling_and_level() {
327 let function = function1();
328 let expected = quote! {
329 #[cfg_attr(
330 feature = "tracing",
331 tracing::instrument( fields(servo_profiling = true), level = "trace",)
332 )]
333 };
334
335 let test_case = quote! {
336 #[servo_tracing::instrument(fields(servo_profiling = true),level = "trace",)]
337 };
338 evaluate(function, test_case, expected);
339 }
340
341 #[test]
342 fn passing_servo_profiling() {
343 let function = function1();
344 let expected = quote! {
345 #[cfg_attr(
346 feature = "tracing",
347 tracing::instrument( fields(servo_profiling = true), level = "trace",)
348 )]
349 };
350
351 let test_case = quote! {
352 #[servo_tracing::instrument(fields(servo_profiling = true))]
353 };
354 evaluate(function, test_case, expected);
355 }
356
357 #[test]
358 fn inject_level_and_servo_profiling() {
359 let function = function1();
360 let expected = quote! {
361 #[cfg_attr(
362 feature = "tracing",
363 tracing::instrument(fields(servo_profiling = true), level = "trace",)
364 )]
365 };
366
367 let test_case = quote! {
368 #[servo_tracing::instrument()]
369 };
370 evaluate(function, test_case, expected);
371 }
372
373 #[test]
374 fn instrument_with_name() {
375 let function = function2();
376 let expected = quote! {
377 #[cfg_attr(
378 feature = "tracing",
379 tracing::instrument(
380 name = "Table::layout",
381 skip_all,
382 fields(servo_profiling = true),
383 level = "trace",
384 )
385 )]
386 };
387
388 let test_case = quote! {
389 #[servo_tracing::instrument(name="Table::layout", skip_all)]
390 };
391
392 evaluate(function, test_case, expected);
393 }
394}