cucumber-codegen 0.19.1

Code generation for `cucumber` crate.
Documentation
// Copyright (c) 2020-2022  Brendan Molloy <brendan@bbqsrc.net>,
//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
//                          Kai Ren <tyranron@gmail.com>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! `#[given]`, `#[when]` and `#[then]` attribute macros implementation.

use std::{iter, mem};

use cucumber_expressions::{Expression, Parameter, SingleExpression, Spanned};
use inflections::case::to_pascal_case;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use regex::{self, Regex};
use syn::{
    parse::{Parse, ParseStream},
    parse_quote,
    spanned::Spanned as _,
};

/// Names of default [`Parameter`]s.
const DEFAULT_PARAMETERS: [&str; 5] = ["int", "float", "word", "string", ""];

/// Generates code of `#[given]`, `#[when]` and `#[then]` attribute macros
/// expansion.
pub(crate) fn step(
    attr_name: &'static str,
    args: TokenStream,
    input: TokenStream,
) -> syn::Result<TokenStream> {
    Step::parse(attr_name, args, input).and_then(Step::expand)
}

/// Parsed state (ready for code generation) of the attribute and the function
/// it's applied to.
#[derive(Clone, Debug)]
struct Step {
    /// Name of the attribute (`given`, `when` or `then`).
    attr_name: &'static str,

    /// Argument of the attribute.
    attr_arg: AttributeArgument,

    /// Function the attribute is applied to.
    func: syn::ItemFn,

    /// Name of the function argument representing a [`gherkin::Step`]
    /// reference.
    ///
    /// [`gherkin::Step`]: https://bit.ly/3j42hcd
    step_arg_name: Option<syn::Ident>,
}

impl Step {
    /// Parses [`Step`] definition from the attribute macro input.
    fn parse(
        attr_name: &'static str,
        attr: TokenStream,
        body: TokenStream,
    ) -> syn::Result<Self> {
        let attr_arg = syn::parse2::<AttributeArgument>(attr)?;
        let mut func = syn::parse2::<syn::ItemFn>(body)?;

        let step_arg_name = {
            let (arg_marked_as_step, _) =
                remove_all_attrs_if_needed("step", &mut func);

            match arg_marked_as_step.len() {
                0 => Ok(None),
                1 => {
                    let (ident, _) = parse_fn_arg(arg_marked_as_step[0])?;
                    Ok(Some(ident.clone()))
                }
                _ => Err(syn::Error::new(
                    arg_marked_as_step[1].span(),
                    "Only 1 step argument is allowed",
                )),
            }
        }?
        .or_else(|| {
            func.sig.inputs.iter().find_map(|arg| {
                if let Ok((ident, _)) = parse_fn_arg(arg) {
                    if ident == "step" {
                        return Some(ident.clone());
                    }
                }
                None
            })
        });

        Ok(Self {
            attr_name,
            attr_arg,
            func,
            step_arg_name,
        })
    }

    /// Expands generated code of this [`Step`] definition.
    fn expand(self) -> syn::Result<TokenStream> {
        let func = &self.func;
        let func_name = &func.sig.ident;

        let world = parse_world_from_args(&self.func.sig)?;
        let step_type = self.step_type();
        let (func_args, addon_parsing) =
            self.fn_arguments_and_additional_parsing()?;

        let regex = self.gen_regex()?;

        let awaiting = func.sig.asyncness.map(|_| quote! { .await });
        let unwrapping = (!self.returns_unit())
            .then(|| quote! { .unwrap_or_else(|e| panic!("{}", e)) });

        Ok(quote! {
            #func

            #[automatically_derived]
            ::cucumber::codegen::submit!({
                // TODO: Remove this, once `#![feature(more_qualified_paths)]`
                //       is stabilized:
                //       https://github.com/rust-lang/rust/issues/86935
                type StepAlias =
                    <#world as ::cucumber::codegen::WorldInventory>::#step_type;

                StepAlias {
                    loc: ::cucumber::step::Location {
                        path: ::std::file!(),
                        line: ::std::line!(),
                        column: ::std::column!(),
                    },
                    regex: || {
                        static LAZY: ::cucumber::codegen::Lazy<
                            ::cucumber::codegen::Regex
                        > = ::cucumber::codegen::Lazy::new(|| { #regex });
                        LAZY.clone()
                    },
                    func: |__cucumber_world, __cucumber_ctx| {
                        let f = async move {
                            #addon_parsing
                            let _ = #func_name(__cucumber_world, #func_args)
                                #awaiting
                                #unwrapping;
                        };
                        ::std::boxed::Box::pin(f)
                    },
                }
            });
        })
    }

    /// Indicates whether this [`Step::func`] return type is `()`.
    fn returns_unit(&self) -> bool {
        match &self.func.sig.output {
            syn::ReturnType::Default => true,
            syn::ReturnType::Type(_, ty) => {
                if let syn::Type::Tuple(syn::TypeTuple { elems, .. }) = &**ty {
                    elems.is_empty()
                } else {
                    false
                }
            }
        }
    }

    /// Generates code that prepares function's arguments basing on
    /// [`AttributeArgument`] and additional parsing if it's an
    /// [`AttributeArgument::Regex`].
    fn fn_arguments_and_additional_parsing(
        &self,
    ) -> syn::Result<(TokenStream, Option<TokenStream>)> {
        let is_regex_or_expr = matches!(
            self.attr_arg,
            AttributeArgument::Regex(_) | AttributeArgument::Expression(_),
        );
        let func = &self.func;

        if is_regex_or_expr {
            if let Some(elem_ty) = find_first_slice(&func.sig) {
                let addon_parsing = Some(quote! {
                    let mut __cucumber_matches = ::std::vec::Vec::with_capacity(
                        __cucumber_ctx.matches.len().saturating_sub(1),
                    );
                    let mut __cucumber_iter = __cucumber_ctx
                        .matches
                        .iter()
                        .skip(1)
                        .enumerate();
                    while let Some((i, (cap_name, s))) =
                        __cucumber_iter.next()
                    {
                        // Special handling of `cucumber-expressions`
                        // `parameter` with multiple capturing groups.
                        let prefix = cap_name
                            .as_ref()
                            .filter(|n| n.starts_with("__"))
                            .map(|n| {
                                let num_len = n
                                    .chars()
                                    .skip(2)
                                    .take_while(|&c| c != '_')
                                    .map(char::len_utf8)
                                    .sum::<usize>();
                                let len = num_len + b"__".len();
                                n.split_at(len).0
                            });

                        let to_take = __cucumber_iter
                            .clone()
                            .take_while(|(_, (n, _))| {
                                prefix
                                    .zip(n.as_ref())
                                    .filter(|(prefix, n)| n.starts_with(prefix))
                                    .is_some()
                            })
                            .count();

                        let s = ::std::iter::once(s.as_str())
                            .chain(
                                __cucumber_iter
                                    .by_ref()
                                    .take(to_take)
                                    .map(|(_, (_, s))| s.as_str()),
                            )
                            .fold(None, |acc, s| {
                                acc.or_else(|| (!s.is_empty()).then_some(s))
                            })
                            .unwrap_or_default();

                        __cucumber_matches.push(
                            s.parse::<#elem_ty>().unwrap_or_else(|e| panic!(
                                "Failed to parse element at {} '{}': {}",
                                i, s, e,
                            ))
                        );
                    }
                });
                let func_args = func
                    .sig
                    .inputs
                    .iter()
                    .skip(1)
                    .map(|arg| self.borrow_step_or_slice(arg))
                    .collect::<Result<TokenStream, _>>()?;

                Ok((func_args, addon_parsing))
            } else {
                // false positive: impl of `FnOnce` is not general enough
                #[allow(clippy::redundant_closure_for_method_calls)]
                let (idents, parsings): (Vec<_>, Vec<_>) =
                    itertools::process_results(
                        func.sig
                            .inputs
                            .iter()
                            .skip(1)
                            .map(|arg| self.arg_ident_and_parse_code(arg)),
                        |i| i.unzip(),
                    )?;

                let addon_parsing = Some(quote! {
                    let mut __cucumber_iter = __cucumber_ctx
                        .matches.iter()
                        .skip(1);
                    #( #parsings )*
                });
                let func_args = quote! {
                    #( #idents, )*
                };

                Ok((func_args, addon_parsing))
            }
        } else if self.step_arg_name.is_some() {
            Ok((
                quote! { ::std::borrow::Borrow::borrow(&__cucumber_ctx.step), },
                None,
            ))
        } else {
            Ok((TokenStream::default(), None))
        }
    }

    /// Composes a name of the `cucumber::codegen::WorldInventory` associated
    /// type to wire this [`Step`] with.
    fn step_type(&self) -> syn::Ident {
        format_ident!("{}", to_pascal_case(self.attr_name))
    }

    /// Returns [`syn::Ident`] and parsing code of the given function's
    /// argument.
    ///
    /// Function's argument type have to implement [`FromStr`].
    ///
    /// [`FromStr`]: std::str::FromStr
    /// [`syn::Ident`]: struct@syn::Ident
    fn arg_ident_and_parse_code<'a>(
        &self,
        arg: &'a syn::FnArg,
    ) -> syn::Result<(&'a syn::Ident, TokenStream)> {
        let (ident, ty) = parse_fn_arg(arg)?;

        let is_ctx_arg =
            self.step_arg_name.as_ref().map(|i| *i == *ident) == Some(true);

        let decl = if is_ctx_arg {
            quote! {
                let #ident =
                    ::std::borrow::Borrow::borrow(&__cucumber_ctx.step);
            }
        } else {
            let ty = if let syn::Type::Path(p) = ty {
                p
            } else {
                return Err(syn::Error::new(ty.span(), "Type path expected"));
            };

            let not_found_err = format!("{ident} not found");
            let parsing_err = format!(
                "{ident} can not be parsed to {}",
                ty.path
                    .segments
                    .last()
                    .ok_or_else(|| {
                        syn::Error::new(ty.path.span(), "Type path expected")
                    })?
                    .ident,
            );

            quote! {
                let #ident = {
                    let (cap_name, s) = __cucumber_iter
                        .next()
                        .expect(#not_found_err);
                    // Special handling of `cucumber-expressions` `parameter`
                    // with multiple capturing groups.
                    let prefix = cap_name
                        .as_ref()
                        .filter(|n| n.starts_with("__"))
                        .map(|n| {
                            let num_len = n
                                .chars()
                                .skip(2)
                                .take_while(|&c| c != '_')
                                .map(char::len_utf8)
                                .sum::<usize>();
                            let len = num_len + b"__".len();
                            n.split_at(len).0
                        });

                    let to_take = __cucumber_iter
                        .clone()
                        .take_while(|(n, _)| {
                            prefix.zip(n.as_ref())
                                .filter(|(prefix, n)| n.starts_with(prefix))
                                .is_some()
                        })
                        .count();

                    ::std::iter::once(s.as_str())
                        .chain(
                            __cucumber_iter
                                .by_ref()
                                .take(to_take)
                                .map(|(_, s)| s.as_str()),
                        )
                        .fold(None, |acc, s| {
                            acc.or_else(|| (!s.is_empty()).then_some(s))
                        })
                        .unwrap_or_default()
                };
                let #ident = #ident.parse::<#ty>().expect(#parsing_err);
            }
        };

        Ok((ident, decl))
    }

    /// Generates code that borrows [`gherkin::Step`] from context if the given
    /// `arg` matches `step_arg_name`, or else borrows parsed slice.
    ///
    /// [`gherkin::Step`]: https://bit.ly/3j42hcd
    fn borrow_step_or_slice(
        &self,
        arg: &syn::FnArg,
    ) -> syn::Result<TokenStream> {
        if let Some(name) = &self.step_arg_name {
            let (ident, _) = parse_fn_arg(arg)?;
            if name == ident {
                return Ok(quote! {
                    ::std::borrow::Borrow::borrow(&__cucumber_ctx.step),
                });
            }
        }

        Ok(quote! {
            __cucumber_matches.as_slice(),
        })
    }

    /// Generates code constructing a [`Regex`] based on an
    /// [`AttributeArgument`].
    ///
    /// # Errors
    ///
    /// - If [`AttributeArgument::Regex`] isn't a valid [`Regex`].
    /// - If [`AttributeArgument::Expression`] passed to
    ///   [`gen_expression_regex()`] errors.
    ///
    /// [`gen_expression_regex()`]: Self::gen_expression_regex
    fn gen_regex(&self) -> syn::Result<TokenStream> {
        match &self.attr_arg {
            AttributeArgument::Literal(l) => {
                let lit = syn::LitStr::new(
                    &format!("^{}$", regex::escape(&l.value())),
                    l.span(),
                );

                Ok(quote! { ::cucumber::codegen::Regex::new(#lit).unwrap() })
            }
            AttributeArgument::Regex(re) => {
                drop(Regex::new(re.value().as_str()).map_err(|e| {
                    syn::Error::new(re.span(), format!("Invalid regex: {e}"))
                })?);

                Ok(quote! { ::cucumber::codegen::Regex::new(#re).unwrap() })
            }
            AttributeArgument::Expression(expr) => {
                self.gen_expression_regex(expr)
            }
        }
    }

    /// Generates code constructing [`Regex`] for an
    /// [`AttributeArgument::Expression`].
    ///
    /// # Errors
    ///
    /// If [`Parameters::new()`] errors.
    fn gen_expression_regex(
        &self,
        expr: &syn::LitStr,
    ) -> syn::Result<TokenStream> {
        let expr = expr.value();
        let params =
            Parameters::new(&expr, &self.func, self.step_arg_name.as_ref())?;

        let provider_impl =
            params.gen_provider_impl(&parse_quote! { Provider });
        let const_assertions = params.gen_const_assertions();

        Ok(quote! {{
            #const_assertions

            #[automatically_derived]
            #[derive(Clone, Copy)]
            struct Provider;

            #provider_impl

            // This should never fail because:
            // 1. We checked AST correctness with `Expression::parse()`;
            // 2. Custom `Parameter::REGEX`es are correct due to be validated
            //    in `#[derive(Parameter)]` macro expansion;
            // 3. All the parameter names are equal to the corresponding
            //    function arguments, so we shouldn't see any
            //    `UnknownParameterError`s.
            ::cucumber::codegen::Expression::regex_with_parameters(
                #expr,
                Provider,
            )
            .unwrap()
        }})
    }
}

/// [`Parameter`] parsed from an [`AttributeArgument::Expression`] along with a
/// [`fn`] argument's [`syn::Type`] corresponding to it.
struct ParameterProvider<'p> {
    /// [`Parameter`] parsed from an [`AttributeArgument::Expression`].
    param: Parameter<Spanned<'p>>,

    /// [`syn::Type`] of the [`fn`] argument corresponding to the [`Parameter`].
    ty: syn::Type,
}

/// Collection of [`ParameterProvider`]s.
struct Parameters<'p>(Vec<ParameterProvider<'p>>);

impl<'p> Parameters<'p> {
    /// Creates new [`Parameters`].
    ///
    /// # Errors
    ///
    /// - If [`Expression::parse()`] errors.
    /// - If [`parse_fn_arg()`] on one of the `func`'s arguments errors.
    /// - If non-default [`Parameter`] doesn't have the corresponding `func`'s
    ///   argument.
    fn new(
        expr: &'p str,
        func: &syn::ItemFn,
        step: Option<&syn::Ident>,
    ) -> syn::Result<Self> {
        let expr = Expression::parse(expr).map_err(|e| {
            syn::Error::new(
                expr.span(),
                format!("Incorrect cucumber expression: {e}"),
            )
        })?;

        let param_tys = func
            .sig
            .inputs
            .iter()
            .skip(1)
            .filter_map(|arg| {
                let (ident, ty) = match parse_fn_arg(arg) {
                    Ok(res) => res,
                    Err(err) => return Some(Err(err)),
                };
                let is_step = step.map(|s| s == ident).unwrap_or_default();
                (!is_step).then_some(Ok(ty))
            })
            .collect::<syn::Result<Vec<_>>>()?;

        expr.0
            .into_iter()
            .filter_map(|e| match e {
                SingleExpression::Parameter(par) => Some(par),
                SingleExpression::Alternation(_)
                | SingleExpression::Optional(_)
                | SingleExpression::Text(_)
                | SingleExpression::Whitespaces(_) => None,
            })
            .zip(param_tys.into_iter().map(Some).chain(iter::repeat(None)))
            .filter_map(|(ast, param_ty)| {
                if DEFAULT_PARAMETERS.iter().any(|s| s == &**ast) {
                    // If parameter is default, it's OK if there is no type
                    // corresponding to it, as we know its regex.
                    param_ty
                        .cloned()
                        .map(|ty| Ok(ParameterProvider { param: ast, ty }))
                } else if let Some(ty) = param_ty.cloned() {
                    Some(Ok(ParameterProvider { param: ast, ty }))
                } else {
                    Some(Err(syn::Error::new(
                        func.sig.inputs.span(),
                        format!(
                            "Function argument corresponding to the `{{{p}}}` \
                             parameter isn't found. Consider adding \
                             argument implementing a `Parameter` trait with \
                             `Parameter::NAME == {p}`.",
                            p = *ast,
                        ),
                    )))
                }
            })
            .collect::<syn::Result<Vec<_>>>()
            .map(Self)
    }

    /// Generates code asserting that all the corresponding
    /// [`ParameterProvider::param`]s and [`ParameterProvider::ty`]s are
    /// correct.
    ///
    /// Here `correct` means one of 2 things:
    /// 1. If a [`ParameterProvider::param`] is one of [`DEFAULT_PARAMETERS`],
    ///    then its [`ParameterProvider::ty`] shouldn't implement a `Parameter`
    ///    trait, Because in case it does, there is a special `Parameter::NAME`,
    ///    that should be used instead of the default one, while it cannot be
    ///    done.
    /// 2. If a [`ParameterProvider::param`] isn't one of
    ///    [`DEFAULT_PARAMETERS`], then its [`ParameterProvider::ty`] must
    ///    implement a `Parameter` trait with
    ///    `Parameter::NAME == `[`ParameterProvider::param`].
    fn gen_const_assertions(&self) -> TokenStream {
        self.0
            .iter()
            .map(|par| {
                let name = par.param.input.fragment();
                let ty = &par.ty;

                if DEFAULT_PARAMETERS.contains(name) {
                    // We do use here custom machinery, rather than using
                    // existing one from `const_assertions` crate, for the
                    // purpose of better errors reporting when the assertion
                    // fails.

                    let trait_with_hint = format_ident!(
                        "UseParameterNameInsteadOf{}",
                        to_pascal_case(name),
                    );

                    quote! {
                        // In case we encounter default parameter, we should
                        // assert that corresponding argument's type __doesn't__
                        // implement a `Parameter` trait.
                        // TODO: Try to use autoderef-based specialization with
                        //       readable assertion message.
                        #[automatically_derived]
                        const _: fn() = || {
                            // Generic trait with a blanket impl over `()` for
                            // all types.
                            #[automatically_derived]
                            trait #trait_with_hint<A> {
                                fn method() {}
                            }

                            #[automatically_derived]
                            impl<T: ?Sized> #trait_with_hint<()> for T {}

                            // Used for the specialized impl when `Parameter` is
                            // implemented.
                            #[automatically_derived]
                            #[allow(dead_code)]
                            struct Invalid;

                            #[automatically_derived]
                            impl<T: ?Sized + ::cucumber::Parameter>
                                #trait_with_hint<Invalid> for T {}

                            // If there is only one specialized trait impl, type
                            // inference with `_` can be resolved and this can
                            // compile. Fails to compile if `#ty` implements
                            // `#trait_with_hint<Invalid>`.
                            let _: fn() = <#ty as #trait_with_hint<_>>::method;
                        };
                    }
                } else {
                    // Here we use double escaping to properly render `{name}`
                    // in the assertion message of the generated code.
                    let assert_msg = format!(
                        "Type `{}` doesn't implement a custom parameter \
                         `{{{{{name}}}}}`",
                        quote! { #ty },
                    );

                    quote! {
                        // In case we encounter a custom parameter, we should
                        // assert that the corresponding type implements
                        // `Parameter` and has correct `Parameter::NAME`.
                        #[automatically_derived]
                        const _: () = ::std::assert!(
                            ::cucumber::codegen::str_eq(
                                <#ty as ::cucumber::Parameter>::NAME,
                                #name,
                            ),
                            #assert_msg,
                        );
                    }
                }
            })
            .collect()
    }

    /// Generates code implementing a [`Provider`] for the given `ty`pe.
    ///
    /// [`Provider`]: cucumber_expressions::expand::parameters::Provider
    fn gen_provider_impl(&self, ty: &syn::Type) -> TokenStream {
        let (custom_par, custom_par_ty): (Vec<_>, Vec<_>) = self
            .0
            .iter()
            .filter_map(|par| {
                let name = par.param.input.fragment();
                (!DEFAULT_PARAMETERS.contains(name)).then_some((*name, &par.ty))
            })
            .unzip();

        quote! {
            #[automatically_derived]
            impl<'s> ::cucumber::codegen::ParametersProvider<
                ::cucumber::codegen::Spanned<'s>
            > for #ty {
                type Item = char;
                type Value = &'static str;

                fn get(
                    &self,
                    input: &::cucumber::codegen::Spanned<'s>,
                ) -> ::std::option::Option<Self::Value> {
                    #( if *input.fragment() == #custom_par {
                        ::std::option::Option::Some(
                            <#custom_par_ty as ::cucumber::Parameter>::REGEX,
                        )
                    } else )* {
                        ::std::option::Option::None
                    }
                }
            }
        }
    }
}

/// Argument of the attribute macro.
#[derive(Clone, Debug)]
enum AttributeArgument {
    /// `#[step("literal")]` case.
    Literal(syn::LitStr),

    /// `#[step(regex = "regex")]` case.
    Regex(syn::LitStr),

    /// `#[step(expr = "cucumber-expression")]` case.
    Expression(syn::LitStr),
}

impl Parse for AttributeArgument {
    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
        let arg = input.parse::<syn::NestedMeta>()?;
        match arg {
            syn::NestedMeta::Meta(syn::Meta::NameValue(arg)) => {
                match arg.path.get_ident() {
                    Some(i) if i == "regex" => {
                        Ok(Self::Regex(to_string_literal(arg.lit)?))
                    }
                    Some(i) if i == "expr" => {
                        Ok(Self::Expression(to_string_literal(arg.lit)?))
                    }
                    _ => Err(syn::Error::new(
                        arg.span(),
                        "Expected `regex` or `expr` argument",
                    )),
                }
            }

            syn::NestedMeta::Lit(l) => Ok(Self::Literal(to_string_literal(l)?)),

            syn::NestedMeta::Meta(_) => Err(syn::Error::new(
                arg.span(),
                "Expected string literal, `regex` or `expr` argument",
            )),
        }
    }
}

/// Removes all `#[attr_arg]` attributes from the given function signature and
/// returns these attributes along with the corresponding function's arguments
/// in case there are no more `#[given]`, `#[when]` or `#[then]` attributes.
fn remove_all_attrs_if_needed<'a>(
    attr_arg: &str,
    func: &'a mut syn::ItemFn,
) -> (Vec<&'a syn::FnArg>, Vec<syn::Attribute>) {
    let has_other_step_arguments = func.attrs.iter().any(|attr| {
        attr.path
            .segments
            .last()
            .map(|segment| {
                ["given", "when", "then"]
                    .iter()
                    .any(|step| segment.ident == step)
            })
            .unwrap_or_default()
    });

    func.sig
        .inputs
        .iter_mut()
        .filter_map(|arg| {
            if has_other_step_arguments {
                find_attr(attr_arg, arg)
            } else {
                remove_attr(attr_arg, arg)
            }
            .map(move |attr| (&*arg, attr))
        })
        .unzip()
}

/// Finds attribute `#[attr_arg]` from function's argument, if any.
fn find_attr(attr_arg: &str, arg: &mut syn::FnArg) -> Option<syn::Attribute> {
    if let syn::FnArg::Typed(typed_arg) = arg {
        typed_arg
            .attrs
            .iter()
            .find(|attr| {
                attr.path
                    .get_ident()
                    .map(|ident| ident == attr_arg)
                    .unwrap_or_default()
            })
            .cloned()
    } else {
        None
    }
}

/// Removes attribute `#[attr_arg]` from function's argument, if any.
fn remove_attr(attr_arg: &str, arg: &mut syn::FnArg) -> Option<syn::Attribute> {
    use itertools::{Either, Itertools as _};

    if let syn::FnArg::Typed(typed_arg) = arg {
        let attrs = mem::take(&mut typed_arg.attrs);

        let (mut other, mut removed): (Vec<_>, Vec<_>) =
            attrs.into_iter().partition_map(|attr| {
                if let Some(ident) = attr.path.get_ident() {
                    if ident == attr_arg {
                        return Either::Right(attr);
                    }
                }
                Either::Left(attr)
            });

        if removed.len() == 1 {
            typed_arg.attrs = other;
            return removed.pop();
        }
        other.append(&mut removed);
        typed_arg.attrs = other;
    }
    None
}

/// Parses [`syn::Ident`] and [`syn::Type`] from the given [`syn::FnArg`].
///
/// [`syn::Ident`]: struct@syn::Ident
fn parse_fn_arg(arg: &syn::FnArg) -> syn::Result<(&syn::Ident, &syn::Type)> {
    let arg = match arg {
        syn::FnArg::Typed(t) => t,
        syn::FnArg::Receiver(_) => {
            return Err(syn::Error::new(
                arg.span(),
                "Expected regular argument, found `self`",
            ))
        }
    };

    let syn::Pat::Ident(syn::PatIdent{ ident, .. }) = arg.pat.as_ref() else {
        return Err(syn::Error::new(arg.span(), "Expected ident"));
    };

    Ok((ident, arg.ty.as_ref()))
}

/// Parses type of a first slice element of the given function signature.
fn find_first_slice(sig: &syn::Signature) -> Option<&syn::TypePath> {
    sig.inputs.iter().find_map(|arg| {
        match arg {
            syn::FnArg::Typed(typed_arg) => Some(typed_arg),
            syn::FnArg::Receiver(_) => None,
        }
        .and_then(|typed_arg| {
            if let syn::Type::Reference(r) = typed_arg.ty.as_ref() {
                Some(r)
            } else {
                None
            }
            .and_then(|ty_ref| {
                if let syn::Type::Slice(s) = ty_ref.elem.as_ref() {
                    Some(s)
                } else {
                    None
                }
                .and_then(|slice| {
                    if let syn::Type::Path(ty) = slice.elem.as_ref() {
                        Some(ty)
                    } else {
                        None
                    }
                })
            })
        })
    })
}

/// Parses `cucumber::World` from arguments of the function signature.
fn parse_world_from_args(sig: &syn::Signature) -> syn::Result<&syn::TypePath> {
    sig.inputs
        .first()
        .ok_or_else(|| sig.ident.span())
        .and_then(|first_arg| match first_arg {
            syn::FnArg::Typed(a) => Ok(a),
            syn::FnArg::Receiver(_) => Err(first_arg.span()),
        })
        .and_then(|typed_arg| {
            if let syn::Type::Reference(r) = typed_arg.ty.as_ref() {
                Ok(r)
            } else {
                Err(typed_arg.span())
            }
        })
        .and_then(|world_ref| match world_ref.mutability {
            Some(_) => Ok(world_ref),
            None => Err(world_ref.span()),
        })
        .and_then(|world_mut_ref| {
            if let syn::Type::Path(p) = world_mut_ref.elem.as_ref() {
                Ok(p)
            } else {
                Err(world_mut_ref.span())
            }
        })
        .map_err(|span| {
            syn::Error::new(
                span,
                "First function argument expected to be `&mut World`",
            )
        })
}

/// Converts [`syn::Lit`] to [`syn::LitStr`] if possible.
///
/// [`syn::Lit`]: enum@syn::Lit
/// [`syn::LitStr`]: struct@syn::LitStr
fn to_string_literal(l: syn::Lit) -> syn::Result<syn::LitStr> {
    if let syn::Lit::Str(str) = l {
        Ok(str)
    } else {
        Err(syn::Error::new(l.span(), "Expected string literal"))
    }
}