| /* 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<MetaList> 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<Meta, Comma> = |
| 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<Fields> 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<Directive>); |
| |
| 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<Self> { |
| let args = Punctuated::<Meta, Token![,]>::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<proc_macro2::TokenStream> { |
| // 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::<TokenStream>() |
| .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); |
| } |
| } |