diff --git a/fastrace-macro/src/args.rs b/fastrace-macro/src/args.rs new file mode 100644 index 0000000..d69554a --- /dev/null +++ b/fastrace-macro/src/args.rs @@ -0,0 +1,119 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use proc_macro2::Span; +use syn::Error; +use syn::LitBool; +use syn::LitStr; +use syn::Path; +use syn::Result; +use syn::Token; +use syn::braced; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::parse_quote; +use syn::spanned::Spanned; + +#[cfg_attr(not(feature = "enable"), allow(dead_code))] +pub struct Property { + pub key: LitStr, + pub value: LitStr, +} + +impl Parse for Property { + fn parse(input: ParseStream) -> Result { + let key: LitStr = input.parse()?; + input.parse::()?; + let value: LitStr = input.parse()?; + Ok(Property { key, value }) + } +} + +#[cfg_attr(not(feature = "enable"), allow(dead_code))] +pub struct Args { + pub name: Option, + pub short_name: bool, + pub enter_on_poll: bool, + pub properties: Vec, + pub crate_path: Path, +} + +impl Parse for Args { + fn parse(input: ParseStream) -> Result { + let mut name = None; + let mut short_name = false; + let mut enter_on_poll = false; + let mut properties = vec![]; + let mut crate_path = parse_quote!(::fastrace); + let mut seen = HashSet::new(); + + while !input.is_empty() { + let key: Path = input.parse()?; + let key = key + .get_ident() + .ok_or_else(|| Error::new(key.span(), "expected identifier"))?; + if seen.contains(key) { + return Err(Error::new(key.span(), "duplicate argument")); + } + seen.insert(key.clone()); + input.parse::()?; + match key.to_string().as_str() { + "name" => { + let parsed_name: LitStr = input.parse()?; + name = Some(parsed_name); + } + "short_name" => { + let parsed_short_name: LitBool = input.parse()?; + short_name = parsed_short_name.value; + } + "enter_on_poll" => { + let parsed_enter_on_poll: LitBool = input.parse()?; + enter_on_poll = parsed_enter_on_poll.value; + } + "properties" => { + let content; + let _brace_token = braced!(content in input); + let property_list = content.parse_terminated(Property::parse, Token![,])?; + for property in property_list { + if properties.iter().any(|p: &Property| p.key == property.key) { + return Err(Error::new( + Span::call_site(), + format!("duplicate property key: {}", property.key.value()), + )); + } + properties.push(property); + } + } + "crate" => { + let parsed_crate_path: Path = input.parse()?; + crate_path = parsed_crate_path; + } + _ => return Err(Error::new(Span::call_site(), "unexpected identifier")), + } + if !input.is_empty() { + let _ = input.parse::(); + } + } + + Ok(Args { + name, + short_name, + enter_on_poll, + properties, + crate_path, + }) + } +} diff --git a/fastrace-macro/src/impls.rs b/fastrace-macro/src/impls.rs new file mode 100644 index 0000000..bffe00f --- /dev/null +++ b/fastrace-macro/src/impls.rs @@ -0,0 +1,400 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use proc_macro2::Ident; +use proc_macro2::Span; +use proc_macro2::TokenStream; +use quote::ToTokens; +use quote::quote; +use quote::quote_spanned; +use syn::Block; +use syn::Error; +use syn::Expr; +use syn::ExprAsync; +use syn::ExprCall; +use syn::Generics; +use syn::Item; +use syn::ItemFn; +use syn::LitStr; +use syn::Path; +use syn::Result; +use syn::ReturnType; +use syn::Signature; +use syn::Stmt; +use syn::Token; +use syn::Type; +use syn::TypeInfer; +use syn::parse_quote; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::visit_mut; +use syn::visit_mut::VisitMut; + +use crate::args::Args; + +pub(crate) fn gen_trace(args: Args, input: ItemFn) -> Result { + let func_name = &input.sig.ident; + + // Check for async_trait-like patterns in the block, and instrument + // the future instead of the wrapper. + let func_body = if let Some(internal_fun) = + get_async_trait_info(&input.block, input.sig.asyncness.is_some()) + { + match internal_fun.kind { + // async-trait <= 0.1.43 + AsyncTraitKind::Function => { + unimplemented!( + "Please upgrade the crate `async-trait` to a version higher than 0.1.44" + ) + } + // async-trait >= 0.1.44 + AsyncTraitKind::Async(async_expr) => { + let instrumented_block = + gen_block(func_name, &async_expr.block, true, false, &args, None)?; + let async_attrs = &async_expr.attrs; + quote! { + Box::pin(#(#async_attrs) * #instrumented_block) + } + } + } + } else { + let output_ty = match &input.sig.output { + ReturnType::Type(_, ty) => (**ty).clone(), + ReturnType::Default => parse_quote! { () }, + }; + gen_block( + func_name, + &input.block, + input.sig.asyncness.is_some(), + input.sig.asyncness.is_some(), + &args, + Some(output_ty), + )? + }; + + let ItemFn { + attrs, vis, sig, .. + } = input; + + let Signature { + output: return_type, + inputs: params, + unsafety, + constness, + abi, + ident, + asyncness, + generics: + Generics { + params: gen_params, + where_clause, + .. + }, + .. + } = sig; + + let fn_span = ident.span(); + Ok(quote_spanned!(fn_span=> + #(#attrs) * + #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type + #where_clause + { + #func_body + } + )) +} + +fn gen_span_name(func_name: &Ident, args: &Args) -> Result { + let Args { + name, + short_name, + crate_path, + .. + } = args; + + if let Some(span_name) = name { + if span_name.value().is_empty() { + Err(Error::new(Span::call_site(), "`name` can not be empty")) + } else if *short_name { + Err(Error::new( + Span::call_site(), + "`name` and `short_name` can not be used together", + )) + } else { + Ok(name.into_token_stream()) + } + } else { + if *short_name { + Ok(LitStr::new(&func_name.to_string(), func_name.span()).into_token_stream()) + } else { + Ok(quote!(#crate_path::func_path!())) + } + } +} + +fn gen_properties(args: &Args) -> Result { + if args.properties.is_empty() { + return Ok(quote!()); + } + + if args.enter_on_poll { + return Err(Error::new( + Span::call_site(), + "`enter_on_poll` can not be used with `properties`", + )); + } + + let properties = args.properties.iter().map(|p| { + let k = &p.key; + let v = &p.value; + quote!( + (std::borrow::Cow::from(#k), match format_args!(#v) { + __f => if let Some(__s) = __f.as_str() { + std::borrow::Cow::from(__s) + } else { + std::borrow::Cow::from(std::string::ToString::to_string(&__f)) + } + }) + ) + }); + let properties = Punctuated::<_, Token![,]>::from_iter(properties); + Ok(quote!( + .with_properties(|| [ #properties ]) + )) +} + +fn gen_block( + func_name: &Ident, + block: &Block, + async_context: bool, + async_keyword: bool, + args: &Args, + output_ty: Option, +) -> Result { + let name = gen_span_name(func_name, args)?; + let properties = gen_properties(args)?; + let crate_path = &args.crate_path; + + let output_ty_hint = if let Some(mut ty) = output_ty { + // Replaces `impl Trait` with `_`, so that it can be used as the type + // in the LHS of `let` statements. + struct EraseImplTrait; + + impl VisitMut for EraseImplTrait { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let Type::ImplTrait(..) = ty { + *ty = Type::Infer(TypeInfer { + underscore_token: Token![_](ty.span()), + }); + } else { + visit_mut::visit_type_mut(self, ty); + } + } + } + + EraseImplTrait.visit_type_mut(&mut ty); + ty + } else { + parse_quote!(_) + }; + + // Generate the instrumented function body. + // If the function is an `async fn`, this will wrap it in an async block. + // Otherwise, this will enter the span and then perform the rest of the body. + if async_context { + let block = if args.enter_on_poll { + quote!( + #crate_path::future::FutureExt::enter_on_poll( + async move { #block }, + #name + ) + ) + } else { + quote!( + { + let __span__ = #crate_path::Span::enter_with_local_parent( #name ) #properties; + #crate_path::future::FutureExt::in_span( + async move { + let __ret__: #output_ty_hint = #block; + #[allow(unreachable_code)] + __ret__ + }, + __span__, + ) + } + ) + }; + + if async_keyword { + Ok(quote!( + #block.await + )) + } else { + Ok(block) + } + } else { + if args.enter_on_poll { + Err(Error::new( + Span::call_site(), + "`enter_on_poll` can not be applied on non-async function", + )) + } else { + Ok(quote!( + let __guard__ = #crate_path::local::LocalSpan::enter_with_local_parent( #name ) #properties; + #block + )) + } + } +} + +enum AsyncTraitKind<'a> { + // old construction. Contains the function + Function, + // new construction. Contains a reference to the async block + Async(&'a ExprAsync), +} + +struct AsyncTraitInfo<'a> { + // source statement to be patched + #[expect(unused)] + stmt: &'a Stmt, + kind: AsyncTraitKind<'a>, +} + +/// Get the AST of the inner function we need to hook, if it was generated by `async-trait`. +/// +/// When we are given a function annotated by `async-trait`, that function is only a placeholder +/// that returns a pinned future containing the user logic, and it is that pinned future that needs +/// to be instrumented. Were we to instrument its parent, we would only collect information +/// regarding the allocation of that future, and not its own span of execution. +/// +/// Depending on the version of async-trait, we inspect the block of the function to find if it +/// matches the pattern: +/// +/// ```rust,ignore +/// // for async-trait <=0.1.43 +/// async fn foo<...>(...) {...} +/// Box::pin(foo<...>(...)) +/// +/// // for async-trait >= 0.1.44 +/// Box::pin(async move { ... }) +/// ``` +/// +/// We then return the statement to be instrumented, along with some other information. +/// [`gen_block`] will then be able to use that information to instrument the proper +/// function or future. +/// +/// This follows the approach suggested in https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673. +fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option> { + // Are we in an async context? If yes, this isn't an async_trait-like pattern + if block_is_async { + return None; + } + + // list of async functions declared inside the block + let inside_fns = block.stmts.iter().filter_map(|stmt| { + if let Stmt::Item(Item::Fn(fun)) = &stmt { + // If the function is async, this is a candidate + if fun.sig.asyncness.is_some() { + return Some((stmt, fun)); + } + } + None + }); + + // Last expression of the block + // + // This determines the return value of the block. Thus, if we are working on a function whose + // `trait` or `impl` declaration is annotated by async_trait, this is quite likely the point + // where the future is pinned. + let (last_expr_stmt, last_expr) = block.stmts.iter().rev().find_map(|stmt| { + if let Stmt::Expr(expr, None) = stmt { + Some((stmt, expr)) + } else { + None + } + })?; + + // Is the last expression a function call? + let (outside_func, outside_args) = match last_expr { + Expr::Call(ExprCall { func, args, .. }) => (func, args), + _ => return None, + }; + + // Is it a call to `Box::pin()`? + let path = match outside_func.as_ref() { + Expr::Path(path) => &path.path, + _ => return None, + }; + if !path_to_string(path).ends_with("Box::pin") { + return None; + } + + // Does the call take an argument? + // + // If it doesn't, it's not going to compile anyway, but that's no reason to perform an + // out-of-bounds access + if outside_args.is_empty() { + return None; + } + + // Is the argument to Box::pin an async block that captures its arguments? + if let Expr::Async(async_expr) = &outside_args[0] { + // check that the move 'keyword' is present + async_expr.capture?; + + return Some(AsyncTraitInfo { + stmt: last_expr_stmt, + kind: AsyncTraitKind::Async(async_expr), + }); + } + + // Is the argument to Box::pin a function call itself? + let func = match &outside_args[0] { + Expr::Call(ExprCall { func, .. }) => func, + _ => return None, + }; + + // "stringify" the path of the function called + let func_name = match **func { + Expr::Path(ref func_path) => path_to_string(&func_path.path), + _ => return None, + }; + + // Was that function defined inside the current block? + // + // If so, retrieve the statement where it was declared and the function itself. + let (stmt_func_declaration, _) = inside_fns + .into_iter() + .find(|(_, fun)| fun.sig.ident == func_name)?; + + Some(AsyncTraitInfo { + stmt: stmt_func_declaration, + kind: AsyncTraitKind::Function, + }) +} + +// Return a path as a String +fn path_to_string(path: &Path) -> String { + use std::fmt::Write; + // some heuristic to prevent too many allocations + let mut res = String::with_capacity(path.segments.len() * 5); + for i in 0..path.segments.len() { + write!(res, "{}", path.segments[i].ident).expect("writing to a String should never fail"); + if i < path.segments.len() - 1 { + res.push_str("::"); + } + } + res +} diff --git a/fastrace-macro/src/lib.rs b/fastrace-macro/src/lib.rs index c0e43ac..62d5b1a 100644 --- a/fastrace-macro/src/lib.rs +++ b/fastrace-macro/src/lib.rs @@ -1,24 +1,30 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This crate is derived from [1] under the original license header: // Copyright 2020 TiKV Project Authors. Licensed under Apache-2.0. +// [1]: https://github.com/tikv/minitrace-rust/blob/v0.6.4/minitrace-macro/src/lib.rs -//! An attribute macro designed to eliminate boilerplate code for [`fastrace`](https://crates.io/crates/fastrace). +//! An attribute macro designed to eliminate boilerplate code for [`fastrace`]. +//! +//! [`fastrace`]: https://crates.io/crates/fastrace #![recursion_limit = "256"] -#![cfg_attr(not(feature = "enable"), allow(dead_code))] -#![cfg_attr(not(feature = "enable"), allow(unreachable_code))] -use std::collections::HashSet; - -use proc_macro2::Span; -use quote::ToTokens; -use quote::quote; -use quote::quote_spanned; -use syn::parse::Parse; -use syn::parse::ParseStream; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::*; - -use crate::visit_mut::VisitMut; +mod args; +#[cfg(feature = "enable")] +mod impls; /// An attribute macro designed to eliminate boilerplate code. /// @@ -118,448 +124,23 @@ pub fn trace( ) -> proc_macro::TokenStream { #[cfg(not(feature = "enable"))] { - parse_macro_input!(args as Args); - return item; - } + use syn::parse_macro_input; - let args = parse_macro_input!(args as Args); - let input = parse_macro_input!(item as ItemFn); - match gen_trace(args, input) { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into(), + // simply check the attributes + parse_macro_input!(args as args::Args); + item } -} - -fn gen_trace(args: Args, input: ItemFn) -> Result { - let func_name = &input.sig.ident; - // Check for async_trait-like patterns in the block, and instrument - // the future instead of the wrapper. - let func_body = if let Some(internal_fun) = - get_async_trait_info(&input.block, input.sig.asyncness.is_some()) + #[cfg(feature = "enable")] { - match internal_fun.kind { - // async-trait <= 0.1.43 - AsyncTraitKind::Function => { - unimplemented!( - "Please upgrade the crate `async-trait` to a version higher than 0.1.44" - ) - } - // async-trait >= 0.1.44 - AsyncTraitKind::Async(async_expr) => { - let instrumented_block = - gen_block(func_name, &async_expr.block, true, false, &args, None)?; - let async_attrs = &async_expr.attrs; - quote! { - Box::pin(#(#async_attrs) * #instrumented_block) - } - } - } - } else { - let output_ty = match input.sig.output { - ReturnType::Type(_, ref ty) => (**ty).clone(), - ReturnType::Default => parse_quote! { () }, - }; - gen_block( - func_name, - &input.block, - input.sig.asyncness.is_some(), - input.sig.asyncness.is_some(), - &args, - Some(output_ty), - )? - }; - - let ItemFn { - attrs, vis, sig, .. - } = input; - - let Signature { - output: return_type, - inputs: params, - unsafety, - constness, - abi, - ident, - asyncness, - generics: - Generics { - params: gen_params, - where_clause, - .. - }, - .. - } = sig; - - let fn_span = ident.span(); - Ok(quote_spanned!(fn_span=> - #(#attrs) * - #vis #constness #unsafety #asyncness #abi fn #ident<#gen_params>(#params) #return_type - #where_clause - { - #func_body - } - )) -} - -struct Args { - name: Option, - short_name: bool, - enter_on_poll: bool, - properties: Vec<(LitStr, LitStr)>, - crate_path: Path, -} - -impl Default for Args { - fn default() -> Self { - Self { - name: None, - short_name: false, - enter_on_poll: false, - properties: Vec::new(), - crate_path: parse_quote!(::fastrace), + use syn::ItemFn; + use syn::parse_macro_input; + + let args = parse_macro_input!(args as args::Args); + let input = parse_macro_input!(item as ItemFn); + match impls::gen_trace(args, input) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), } } } - -struct Property { - key: LitStr, - value: LitStr, -} - -impl Parse for Property { - fn parse(input: ParseStream) -> Result { - let key: LitStr = input.parse()?; - input.parse::()?; - let value: LitStr = input.parse()?; - Ok(Property { key, value }) - } -} - -impl Parse for Args { - fn parse(input: ParseStream) -> Result { - let mut name = None; - let mut short_name = false; - let mut enter_on_poll = false; - let mut properties = Vec::new(); - let mut crate_path = parse_quote!(::fastrace); - let mut seen = HashSet::new(); - - while !input.is_empty() { - let key: Path = input.parse()?; - let key = key - .get_ident() - .ok_or_else(|| Error::new(key.span(), "expected identifier"))?; - if seen.contains(key) { - return Err(Error::new(key.span(), "duplicate argument")); - } - seen.insert(key.clone()); - input.parse::()?; - match key.to_string().as_str() { - "name" => { - let parsed_name: LitStr = input.parse()?; - name = Some(parsed_name); - } - "short_name" => { - let parsed_short_name: LitBool = input.parse()?; - short_name = parsed_short_name.value; - } - "enter_on_poll" => { - let parsed_enter_on_poll: LitBool = input.parse()?; - enter_on_poll = parsed_enter_on_poll.value; - } - "properties" => { - let content; - let _brace_token = braced!(content in input); - let property_list = content.parse_terminated(Property::parse, Token![,])?; - for property in property_list { - if properties.iter().any(|(k, _)| k == &property.key) { - return Err(Error::new(Span::call_site(), "duplicate property key")); - } - properties.push((property.key, property.value)); - } - } - "crate" => { - let parsed_crate_path: Path = input.parse()?; - crate_path = parsed_crate_path; - } - _ => return Err(Error::new(Span::call_site(), "unexpected identifier")), - } - if !input.is_empty() { - let _ = input.parse::(); - } - } - - Ok(Args { - name, - short_name, - enter_on_poll, - properties, - crate_path, - }) - } -} - -fn gen_name(func_name: &Ident, args: &Args) -> Result { - let crate_path = &args.crate_path; - match &args.name { - Some(name) if name.value().is_empty() => { - Err(Error::new(Span::call_site(), "`name` can not be empty")) - } - Some(_) if args.short_name => Err(Error::new( - Span::call_site(), - "`name` and `short_name` can not be used together", - )), - Some(name) => Ok(name.into_token_stream()), - None if args.short_name => { - Ok(LitStr::new(&func_name.to_string(), func_name.span()).into_token_stream()) - } - None => Ok(quote!(#crate_path::func_path!())), - } -} - -fn gen_properties(args: &Args) -> Result { - if args.properties.is_empty() { - return Ok(quote!()); - } - - if args.enter_on_poll { - return Err(Error::new( - Span::call_site(), - "`enter_on_poll` can not be used with `properties`", - )); - } - - let properties = args.properties.iter().map(|(k, v)| { - quote!( - (std::borrow::Cow::from(#k), match format_args!(#v) { - __f => if let Some(__s) = __f.as_str() { - std::borrow::Cow::from(__s) - } else { - std::borrow::Cow::from(std::string::ToString::to_string(&__f)) - } - }) - ) - }); - let properties = Punctuated::<_, Token![,]>::from_iter(properties); - Ok(quote!( - .with_properties(|| [ #properties ]) - )) -} - -/// Instrument a block -fn gen_block( - func_name: &Ident, - block: &Block, - async_context: bool, - async_keyword: bool, - args: &Args, - output_ty: Option, -) -> Result { - let name = gen_name(func_name, args)?; - let properties = gen_properties(args)?; - let crate_path = &args.crate_path; - let output_ty_hint = output_ty - .map(erase_impl_trait) - .unwrap_or_else(|| parse_quote! { _ }); - - // Generate the instrumented function body. - // If the function is an `async fn`, this will wrap it in an async block. - // Otherwise, this will enter the span and then perform the rest of the body. - if async_context { - let block = if args.enter_on_poll { - quote!( - #crate_path::future::FutureExt::enter_on_poll( - async move { #block }, - #name - ) - ) - } else { - quote!( - { - let __span__ = #crate_path::Span::enter_with_local_parent( #name ) #properties; - #crate_path::future::FutureExt::in_span( - async move { - let __ret__: #output_ty_hint = #block; - #[allow(unreachable_code)] - __ret__ - }, - __span__, - ) - } - ) - }; - - if async_keyword { - Ok(quote!( - #block.await - )) - } else { - Ok(block) - } - } else { - if args.enter_on_poll { - Err(Error::new( - Span::call_site(), - "`enter_on_poll` can not be applied on non-async function", - )) - } else { - Ok(quote!( - let __guard__ = #crate_path::local::LocalSpan::enter_with_local_parent( #name ) #properties; - #block - )) - } - } -} - -enum AsyncTraitKind<'a> { - // old construction. Contains the function - Function, - // new construction. Contains a reference to the async block - Async(&'a ExprAsync), -} - -struct AsyncTraitInfo<'a> { - // statement that must be patched - _source_stmt: &'a Stmt, - kind: AsyncTraitKind<'a>, -} - -// Get the AST of the inner function we need to hook, if it was generated -// by async-trait. -// When we are given a function annotated by async-trait, that function -// is only a placeholder that returns a pinned future containing the -// user logic, and it is that pinned future that needs to be instrumented. -// Were we to instrument its parent, we would only collect information -// regarding the allocation of that future, and not its own span of execution. -// Depending on the version of async-trait, we inspect the block of the function -// to find if it matches the pattern -// `async fn foo<...>(...) {...}; Box::pin(foo<...>(...))` (<=0.1.43), or if -// it matches `Box::pin(async move { ... }) (>=0.1.44). We the return the -// statement that must be instrumented, along with some other information. -// 'gen_body' will then be able to use that information to instrument the -// proper function/future. -// (this follows the approach suggested in -// https://github.com/dtolnay/async-trait/issues/45#issuecomment-571245673) -fn get_async_trait_info(block: &Block, block_is_async: bool) -> Option> { - // are we in an async context? If yes, this isn't an async_trait-like pattern - if block_is_async { - return None; - } - - // list of async functions declared inside the block - let inside_funs = block.stmts.iter().filter_map(|stmt| { - if let Stmt::Item(Item::Fn(fun)) = &stmt { - // If the function is async, this is a candidate - if fun.sig.asyncness.is_some() { - return Some((stmt, fun)); - } - } - None - }); - - // last expression of the block (it determines the return value - // of the block, so that if we are working on a function whose - // `trait` or `impl` declaration is annotated by async_trait, - // this is quite likely the point where the future is pinned) - let (last_expr_stmt, last_expr) = block.stmts.iter().rev().find_map(|stmt| { - if let Stmt::Expr(expr, None) = stmt { - Some((stmt, expr)) - } else { - None - } - })?; - - // is the last expression a function call? - let (outside_func, outside_args) = match last_expr { - Expr::Call(ExprCall { func, args, .. }) => (func, args), - _ => return None, - }; - - // is it a call to `Box::pin()`? - let path = match outside_func.as_ref() { - Expr::Path(path) => &path.path, - _ => return None, - }; - if !path_to_string(path).ends_with("Box::pin") { - return None; - } - - // Does the call take an argument? If it doesn't, - // it's not going to compile anyway, but that's no reason - // to (try to) perform an out-of-bounds access - if outside_args.is_empty() { - return None; - } - - // Is the argument to Box::pin an async block that - // captures its arguments? - if let Expr::Async(async_expr) = &outside_args[0] { - // check that the move 'keyword' is present - async_expr.capture?; - - return Some(AsyncTraitInfo { - _source_stmt: last_expr_stmt, - kind: AsyncTraitKind::Async(async_expr), - }); - } - - // Is the argument to Box::pin a function call itself? - let func = match &outside_args[0] { - Expr::Call(ExprCall { func, .. }) => func, - _ => return None, - }; - - // "stringify" the path of the function called - let func_name = match **func { - Expr::Path(ref func_path) => path_to_string(&func_path.path), - _ => return None, - }; - - // Was that function defined inside the current block? - // If so, retrieve the statement where it was declared and the function itself - let (stmt_func_declaration, _) = inside_funs - .into_iter() - .find(|(_, fun)| fun.sig.ident == func_name)?; - - Some(AsyncTraitInfo { - _source_stmt: stmt_func_declaration, - kind: AsyncTraitKind::Function, - }) -} - -// Return a path as a String -fn path_to_string(path: &Path) -> String { - use std::fmt::Write; - // some heuristic to prevent too many allocations - let mut res = String::with_capacity(path.segments.len() * 5); - for i in 0..path.segments.len() { - write!(res, "{}", path.segments[i].ident).expect("writing to a String should never fail"); - if i < path.segments.len() - 1 { - res.push_str("::"); - } - } - res -} - -/// Replaces any `impl Trait` with `_` so it can be used as the type in -/// a `let` statement's LHS. -struct ImplTraitEraser; - -impl VisitMut for ImplTraitEraser { - fn visit_type_mut(&mut self, t: &mut Type) { - if let Type::ImplTrait(..) = t { - *t = TypeInfer { - underscore_token: Token![_](t.span()), - } - .into(); - } else { - visit_mut::visit_type_mut(self, t); - } - } -} - -fn erase_impl_trait(mut ty: Type) -> Type { - ImplTraitEraser.visit_type_mut(&mut ty); - ty -} diff --git a/tests/macros/tests/ui/err/has-duplicated-properties.rs b/tests/macros/tests/ui/err/has-duplicated-properties.rs new file mode 100644 index 0000000..d85ef24 --- /dev/null +++ b/tests/macros/tests/ui/err/has-duplicated-properties.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(properties = { "key": "first", "key": "second" })] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/has-duplicated-properties.stderr b/tests/macros/tests/ui/err/has-duplicated-properties.stderr new file mode 100644 index 0000000..d372321 --- /dev/null +++ b/tests/macros/tests/ui/err/has-duplicated-properties.stderr @@ -0,0 +1,7 @@ +error: duplicate property key: key + --> tests/ui/err/has-duplicated-properties.rs:3:1 + | +3 | #[trace(properties = { "key": "first", "key": "second" })] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `trace` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/macros/tests/ui/err/has-unknown-argument.rs b/tests/macros/tests/ui/err/has-unknown-argument.rs new file mode 100644 index 0000000..66cb660 --- /dev/null +++ b/tests/macros/tests/ui/err/has-unknown-argument.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(unknown = true)] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/has-unknown-argument.stderr b/tests/macros/tests/ui/err/has-unknown-argument.stderr new file mode 100644 index 0000000..83186bc --- /dev/null +++ b/tests/macros/tests/ui/err/has-unknown-argument.stderr @@ -0,0 +1,7 @@ +error: unexpected identifier + --> tests/ui/err/has-unknown-argument.rs:3:1 + | +3 | #[trace(unknown = true)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `trace` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/macros/tests/ui/err/name-is-not-string.rs b/tests/macros/tests/ui/err/name-is-not-string.rs new file mode 100644 index 0000000..ced1135 --- /dev/null +++ b/tests/macros/tests/ui/err/name-is-not-string.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(name = true)] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/name-is-not-string.stderr b/tests/macros/tests/ui/err/name-is-not-string.stderr new file mode 100644 index 0000000..7a8f632 --- /dev/null +++ b/tests/macros/tests/ui/err/name-is-not-string.stderr @@ -0,0 +1,5 @@ +error: expected string literal + --> tests/ui/err/name-is-not-string.rs:3:16 + | +3 | #[trace(name = true)] + | ^^^^ diff --git a/tests/macros/tests/ui/err/properties-missing-braces.rs b/tests/macros/tests/ui/err/properties-missing-braces.rs new file mode 100644 index 0000000..68af1aa --- /dev/null +++ b/tests/macros/tests/ui/err/properties-missing-braces.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(properties = "key")] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/properties-missing-braces.stderr b/tests/macros/tests/ui/err/properties-missing-braces.stderr new file mode 100644 index 0000000..aa14069 --- /dev/null +++ b/tests/macros/tests/ui/err/properties-missing-braces.stderr @@ -0,0 +1,5 @@ +error: expected curly braces + --> tests/ui/err/properties-missing-braces.rs:3:22 + | +3 | #[trace(properties = "key")] + | ^^^^^ diff --git a/tests/macros/tests/ui/err/properties-value-non-string.rs b/tests/macros/tests/ui/err/properties-value-non-string.rs new file mode 100644 index 0000000..715a228 --- /dev/null +++ b/tests/macros/tests/ui/err/properties-value-non-string.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(properties = { "key": true })] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/properties-value-non-string.stderr b/tests/macros/tests/ui/err/properties-value-non-string.stderr new file mode 100644 index 0000000..7c88b1e --- /dev/null +++ b/tests/macros/tests/ui/err/properties-value-non-string.stderr @@ -0,0 +1,5 @@ +error: expected string literal + --> tests/ui/err/properties-value-non-string.rs:3:31 + | +3 | #[trace(properties = { "key": true })] + | ^^^^ diff --git a/tests/macros/tests/ui/err/short-name-is-not-bool.rs b/tests/macros/tests/ui/err/short-name-is-not-bool.rs new file mode 100644 index 0000000..5946dea --- /dev/null +++ b/tests/macros/tests/ui/err/short-name-is-not-bool.rs @@ -0,0 +1,6 @@ +use fastrace::trace; + +#[trace(short_name = "true")] +fn f() {} + +fn main() {} diff --git a/tests/macros/tests/ui/err/short-name-is-not-bool.stderr b/tests/macros/tests/ui/err/short-name-is-not-bool.stderr new file mode 100644 index 0000000..a8618b5 --- /dev/null +++ b/tests/macros/tests/ui/err/short-name-is-not-bool.stderr @@ -0,0 +1,5 @@ +error: expected boolean literal + --> tests/ui/err/short-name-is-not-bool.rs:3:22 + | +3 | #[trace(short_name = "true")] + | ^^^^^^ diff --git a/tests/macros/tests/ui/ok/has-generic-signatures.rs b/tests/macros/tests/ui/ok/has-generic-signatures.rs new file mode 100644 index 0000000..ce14d6d --- /dev/null +++ b/tests/macros/tests/ui/ok/has-generic-signatures.rs @@ -0,0 +1,23 @@ +use fastrace::trace; + +#[trace(short_name = true)] +fn sync_generic<'a, T, E>(value: &'a T) -> Result<&'a T, E> +where + T: ?Sized, +{ + Ok(value) +} + +#[trace(short_name = true)] +async fn async_generic(value: T) -> impl AsRef +where + T: Into, +{ + value.into() +} + +#[tokio::main] +async fn main() { + let _ = sync_generic::<_, ()>("value"); + let _ = async_generic("value").await; +} diff --git a/tests/macros/tests/ui/ok/has-methods.rs b/tests/macros/tests/ui/ok/has-methods.rs new file mode 100644 index 0000000..d40156f --- /dev/null +++ b/tests/macros/tests/ui/ok/has-methods.rs @@ -0,0 +1,23 @@ +use fastrace::trace; + +struct Worker(String); + +impl Worker { + #[trace(short_name = true)] + fn sync_method(&self) -> &str { + &self.0 + } + + #[trace(short_name = true)] + async fn async_method(&mut self, suffix: &str) -> String { + self.0.push_str(suffix); + self.0.clone() + } +} + +#[tokio::main] +async fn main() { + let mut worker = Worker(String::from("fast")); + let _ = worker.sync_method(); + let _ = worker.async_method("race").await; +} diff --git a/tests/macros/tests/ui/ok/has-sync-properties.rs b/tests/macros/tests/ui/ok/has-sync-properties.rs new file mode 100644 index 0000000..70eeab6 --- /dev/null +++ b/tests/macros/tests/ui/ok/has-sync-properties.rs @@ -0,0 +1,15 @@ +use fastrace::trace; + +#[derive(Debug)] +struct Input { + value: u64, +} + +#[trace(short_name = true, properties = { "literal": "value", "input": "{input:?}", "escaped": "{{input}}" })] +fn f(input: &Input) -> u64 { + input.value +} + +fn main() { + let _ = f(&Input { value: 7 }); +} diff --git a/tests/macros/tests/ui/ok/has-unsafe-extern.rs b/tests/macros/tests/ui/ok/has-unsafe-extern.rs new file mode 100644 index 0000000..9596c27 --- /dev/null +++ b/tests/macros/tests/ui/ok/has-unsafe-extern.rs @@ -0,0 +1,10 @@ +use fastrace::trace; + +#[trace(short_name = true)] +pub(crate) unsafe extern "C" fn f(value: u32) -> u32 { + value +} + +fn main() { + let _ = unsafe { f(7) }; +}