/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::Punct; use quote::{ToTokens, TokenStreamExt, quote}; use syn::parse::{Parse, Parser}; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{Expr, ItemFn, Meta, MetaList, Token, parse_quote, parse2}; struct Fields(MetaList); impl From for Fields { fn from(value: MetaList) -> Self { Fields(value) } } impl Fields { fn create_with_servo_profiling() -> Self { Fields(parse_quote! { fields(servo_profiling = true) }) } fn inject_servo_profiling(&mut self) -> syn::Result<()> { let metalist = std::mem::replace(&mut self.0, parse_quote! {field()}); let arguments: Punctuated = Punctuated::parse_terminated.parse2(metalist.tokens)?; let servo_profile_given = arguments .iter() .any(|arg| arg.path().is_ident("servo_profiling")); let metalist = if servo_profile_given { parse_quote! { fields(#arguments) } } else { parse_quote! { fields(servo_profiling=true, #arguments) } }; let _ = std::mem::replace(&mut self.0, metalist); Ok(()) } } impl ToTokens for Fields { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let items = &self.0; tokens.append_all(quote! { #items }); } } enum Directive { Passthrough(Meta), Level(Expr), Fields(Fields), } impl From for Directive { fn from(value: Fields) -> Self { Directive::Fields(value) } } impl Directive { fn is_level(&self) -> bool { matches!(self, Directive::Level(..)) } fn fields_mut(&mut self) -> Option<&mut Fields> { match self { Directive::Fields(fields) => Some(fields), _ => None, } } } impl ToTokens for Directive { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { match self { Directive::Passthrough(meta) => tokens.append_all(quote! { #meta }), Directive::Level(level) => tokens.append_all(quote! { level = #level }), Directive::Fields(fields) => tokens.append_all(quote! { #fields }), }; } } impl ToTokens for InstrumentConfiguration { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { tokens.append_terminated(&self.0, Punct::new(',', proc_macro2::Spacing::Joint)); } } struct InstrumentConfiguration(Vec); impl InstrumentConfiguration { fn inject_servo_profiling(&mut self) -> syn::Result<()> { let fields = self.0.iter_mut().find_map(Directive::fields_mut); match fields { None => { self.0 .push(Directive::from(Fields::create_with_servo_profiling())); Ok(()) }, Some(fields) => fields.inject_servo_profiling(), } } fn inject_level(&mut self) { if self.0.iter().any(|a| a.is_level()) { return; } self.0.push(Directive::Level(parse_quote! { "trace" })); } } impl Parse for InstrumentConfiguration { fn parse(input: syn::parse::ParseStream) -> syn::Result { let args = Punctuated::::parse_terminated(input)?; let mut components = vec![]; for arg in args { match arg { Meta::List(meta_list) if meta_list.path.is_ident("fields") => { components.push(Directive::Fields(meta_list.into())); }, Meta::NameValue(meta_name_value) if meta_name_value.path.is_ident("level") => { components.push(Directive::Level(meta_name_value.value)); }, _ => { components.push(Directive::Passthrough(arg)); }, } } Ok(InstrumentConfiguration(components)) } } fn instrument_internal( attr: proc_macro2::TokenStream, item: proc_macro2::TokenStream, ) -> syn::Result { // Prepare passthrough arguments for tracing::instrument let mut configuration: InstrumentConfiguration = parse2(attr)?; let input_fn: ItemFn = parse2(item)?; configuration.inject_servo_profiling()?; configuration.inject_level(); let output = quote! { #[cfg_attr( feature = "tracing", tracing::instrument( #configuration ) )] #input_fn }; Ok(output) } #[proc_macro_attribute] /// Instruments a function with some sane defaults by automatically: /// - setting the attribute behind the "tracing" flag /// - adding `servo_profiling = true` in the `tracing::instrument(fields(...))` argument. /// - setting `level = "trace"` if it is not given. /// /// This macro assumes the consuming crate has a `tracing` feature flag. /// /// We need to be able to set the following /// ``` /// #[cfg_attr( /// feature = "tracing", /// tracing::instrument( /// name = "MyCustomName", /// skip_all, /// fields(servo_profiling = true), /// level = "trace", /// ) /// )] /// fn my_fn() { /* .... */ } /// ``` /// from a simpler macro, such as: /// /// ``` /// #[servo_tracing::instrument(name = "MyCustomName", skip_all)] /// fn my_fn() { /* .... */ } /// ``` pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream { match instrument_internal(attr.into(), item.into()) { Ok(stream) => stream.into(), Err(err) => err.to_compile_error().into(), } } #[cfg(test)] mod test { use proc_macro2::TokenStream; use quote::{ToTokens, quote}; use syn::{Attribute, ItemFn}; use crate::instrument_internal; fn extract_instrument_attribute(item_fn: &mut ItemFn) -> TokenStream { let attr: &Attribute = item_fn .attrs .iter() .find(|attr| { // because this is a very nested structure, it is easier to check // by constructing the full path, and then doing a string comparison. let p = attr.path().to_token_stream().to_string(); p == "servo_tracing :: instrument" }) .expect("Attribute `servo_tracing::instrument` not found"); // we create a tokenstream of the actual internal contents of the attribute let attr_args = attr .parse_args::() .expect("Failed to parse attribute args"); // we remove the tracing attribute, this is to avoid passing it as an actual attribute to itself. item_fn.attrs.retain(|attr| { attr.path().to_token_stream().to_string() != "servo_tracing :: instrument" }); attr_args } /// To make test case generation easy, we parse a test_case as a function item /// with its own attributes, including [`servo_tracing::instrument`]. /// /// We extract the [`servo_tracing::instrument`] attribute, and pass it as the first argument to /// [`servo_tracing::instrument_internal`], fn evaluate(function: TokenStream, test_case: TokenStream, expected: TokenStream) { let test_case = quote! { #test_case #function }; let expected = quote! { #expected #function }; let function_str = function.to_string(); let function_str = syn::parse_file(&function_str).expect("function to have valid syntax"); let function_str = prettyplease::unparse(&function_str); let mut item_fn: ItemFn = syn::parse2(test_case).expect("Failed to parse input as function"); let attr_args = extract_instrument_attribute(&mut item_fn); let item_fn = item_fn.to_token_stream(); let generated = instrument_internal(attr_args, item_fn).expect("Generation to not fail."); let generated = syn::parse_file(generated.to_string().as_str()) .expect("to have generated a valid function"); let generated = prettyplease::unparse(&generated); let expected = syn::parse_file(expected.to_string().as_str()) .expect("to have been given a valid expected function"); let expected = prettyplease::unparse(&expected); eprintln!( "Generated:---------:\n{}--------\nExpected:----------\n{}", &generated, &expected ); assert_eq!(generated, expected); assert!( generated.contains(&function_str), "Expected generated code: {generated} to contain the function code: {function_str}" ); } fn function1() -> TokenStream { quote! { pub fn start( state: (), layout_factory: (), random_pipeline_closure_probability: (), random_pipeline_closure_seed: (), hard_fail: (), canvas_create_sender: (), canvas_ipc_sender: (), ) { } } } fn function2() -> TokenStream { quote! { fn layout( mut self, layout_context: &LayoutContext, positioning_context: &mut PositioningContext, containing_block_for_children: &ContainingBlock, containing_block_for_table: &ContainingBlock, depends_on_block_constraints: bool, ) { } } } #[test] fn passing_servo_profiling_and_level_and_aux() { let function = function1(); let expected = quote! { #[cfg_attr( feature = "tracing", tracing::instrument(skip(state, layout_factory), fields(servo_profiling = true), level = "trace",) )] }; let test_case = quote! { #[servo_tracing::instrument(skip(state, layout_factory),fields(servo_profiling = true),level = "trace",)] }; evaluate(function, test_case, expected); } #[test] fn passing_servo_profiling_and_level() { let function = function1(); let expected = quote! { #[cfg_attr( feature = "tracing", tracing::instrument( fields(servo_profiling = true), level = "trace",) )] }; let test_case = quote! { #[servo_tracing::instrument(fields(servo_profiling = true),level = "trace",)] }; evaluate(function, test_case, expected); } #[test] fn passing_servo_profiling() { let function = function1(); let expected = quote! { #[cfg_attr( feature = "tracing", tracing::instrument( fields(servo_profiling = true), level = "trace",) )] }; let test_case = quote! { #[servo_tracing::instrument(fields(servo_profiling = true))] }; evaluate(function, test_case, expected); } #[test] fn inject_level_and_servo_profiling() { let function = function1(); let expected = quote! { #[cfg_attr( feature = "tracing", tracing::instrument(fields(servo_profiling = true), level = "trace",) )] }; let test_case = quote! { #[servo_tracing::instrument()] }; evaluate(function, test_case, expected); } #[test] fn instrument_with_name() { let function = function2(); let expected = quote! { #[cfg_attr( feature = "tracing", tracing::instrument( name = "Table::layout", skip_all, fields(servo_profiling = true), level = "trace", ) )] }; let test_case = quote! { #[servo_tracing::instrument(name="Table::layout", skip_all)] }; evaluate(function, test_case, expected); } }