macros: Refactor ruma_api attributes parsing
This commit is contained in:
		
							parent
							
								
									72fc21c342
								
							
						
					
					
						commit
						734770d2bc
					
				| @ -2,77 +2,147 @@ | ||||
| 
 | ||||
| use syn::{ | ||||
|     parse::{Parse, ParseStream}, | ||||
|     Ident, Lit, Token, Type, | ||||
|     Ident, LitStr, Token, Type, | ||||
| }; | ||||
| 
 | ||||
| /// Value type used for request and response struct attributes
 | ||||
| #[allow(clippy::large_enum_variant)] | ||||
| pub enum MetaValue { | ||||
|     Lit(Lit), | ||||
|     Type(Type), | ||||
| mod kw { | ||||
|     syn::custom_keyword!(body); | ||||
|     syn::custom_keyword!(raw_body); | ||||
|     syn::custom_keyword!(path); | ||||
|     syn::custom_keyword!(query); | ||||
|     syn::custom_keyword!(query_map); | ||||
|     syn::custom_keyword!(header); | ||||
|     syn::custom_keyword!(authentication); | ||||
|     syn::custom_keyword!(method); | ||||
|     syn::custom_keyword!(error_ty); | ||||
|     syn::custom_keyword!(unstable); | ||||
|     syn::custom_keyword!(r0); | ||||
|     syn::custom_keyword!(stable); | ||||
|     syn::custom_keyword!(manual_body_serde); | ||||
| } | ||||
| 
 | ||||
| impl Parse for MetaValue { | ||||
| pub enum RequestMeta { | ||||
|     NewtypeBody, | ||||
|     RawBody, | ||||
|     Path, | ||||
|     Query, | ||||
|     QueryMap, | ||||
|     Header(Ident), | ||||
| } | ||||
| 
 | ||||
| impl Parse for RequestMeta { | ||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { | ||||
|         if input.peek(Lit) { | ||||
|             input.parse().map(Self::Lit) | ||||
|         } else { | ||||
|             input.parse().map(Self::Type) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Like syn::MetaNameValue, but expects an identifier as the value.
 | ||||
| ///
 | ||||
| /// Also, we don't care about the the span of the equals sign, so we don't have the `eq_token` field
 | ||||
| /// from syn::MetaNameValue.
 | ||||
| pub struct MetaNameValue<V> { | ||||
|     /// The part left of the equals sign
 | ||||
|     pub name: Ident, | ||||
| 
 | ||||
|     /// The part right of the equals sign
 | ||||
|     pub value: V, | ||||
| } | ||||
| 
 | ||||
| impl<V: Parse> Parse for MetaNameValue<V> { | ||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { | ||||
|         let ident = input.parse()?; | ||||
|         let _: Token![=] = input.parse()?; | ||||
|         Ok(MetaNameValue { name: ident, value: input.parse()? }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Like syn::Meta, but only parses ruma_api attributes
 | ||||
| pub enum Meta<T> { | ||||
|     /// A single word, like `query` in `#[ruma_api(query)]`
 | ||||
|     Word(Ident), | ||||
| 
 | ||||
|     /// A name-value pair, like `header = CONTENT_TYPE` in `#[ruma_api(header = CONTENT_TYPE)]`
 | ||||
|     NameValue(MetaNameValue<T>), | ||||
| } | ||||
| 
 | ||||
| impl<T: Parse> Meta<T> { | ||||
|     /// Check if the given attribute is a ruma_api attribute.
 | ||||
|     ///
 | ||||
|     /// If it is, parse it.
 | ||||
|     pub fn from_attribute(attr: &syn::Attribute) -> syn::Result<Option<Self>> { | ||||
|         if attr.path.is_ident("ruma_api") { | ||||
|             attr.parse_args().map(Some) | ||||
|         } else { | ||||
|             Ok(None) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<T: Parse> Parse for Meta<T> { | ||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { | ||||
|         let ident = input.parse()?; | ||||
| 
 | ||||
|         if input.peek(Token![=]) { | ||||
|         let lookahead = input.lookahead1(); | ||||
|         if lookahead.peek(kw::body) { | ||||
|             let _: kw::body = input.parse()?; | ||||
|             Ok(Self::NewtypeBody) | ||||
|         } else if lookahead.peek(kw::raw_body) { | ||||
|             let _: kw::raw_body = input.parse()?; | ||||
|             Ok(Self::RawBody) | ||||
|         } else if lookahead.peek(kw::path) { | ||||
|             let _: kw::path = input.parse()?; | ||||
|             Ok(Self::Path) | ||||
|         } else if lookahead.peek(kw::query) { | ||||
|             let _: kw::query = input.parse()?; | ||||
|             Ok(Self::Query) | ||||
|         } else if lookahead.peek(kw::query_map) { | ||||
|             let _: kw::query_map = input.parse()?; | ||||
|             Ok(Self::QueryMap) | ||||
|         } else if lookahead.peek(kw::header) { | ||||
|             let _: kw::header = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             Ok(Meta::NameValue(MetaNameValue { name: ident, value: input.parse()? })) | ||||
|             input.parse().map(Self::Header) | ||||
|         } else { | ||||
|             Ok(Meta::Word(ident)) | ||||
|             Err(lookahead.error()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub enum DeriveRequestMeta { | ||||
|     Authentication(Type), | ||||
|     Method(Type), | ||||
|     ErrorTy(Type), | ||||
|     UnstablePath(LitStr), | ||||
|     R0Path(LitStr), | ||||
|     StablePath(LitStr), | ||||
| } | ||||
| 
 | ||||
| impl Parse for DeriveRequestMeta { | ||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { | ||||
|         let lookahead = input.lookahead1(); | ||||
|         if lookahead.peek(kw::authentication) { | ||||
|             let _: kw::authentication = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::Authentication) | ||||
|         } else if lookahead.peek(kw::method) { | ||||
|             let _: kw::method = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::Method) | ||||
|         } else if lookahead.peek(kw::error_ty) { | ||||
|             let _: kw::error_ty = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::ErrorTy) | ||||
|         } else if lookahead.peek(kw::unstable) { | ||||
|             let _: kw::unstable = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::UnstablePath) | ||||
|         } else if lookahead.peek(kw::r0) { | ||||
|             let _: kw::r0 = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::R0Path) | ||||
|         } else if lookahead.peek(kw::stable) { | ||||
|             let _: kw::stable = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::StablePath) | ||||
|         } else { | ||||
|             Err(lookahead.error()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub enum ResponseMeta { | ||||
|     NewtypeBody, | ||||
|     RawBody, | ||||
|     Header(Ident), | ||||
| } | ||||
| 
 | ||||
| impl Parse for ResponseMeta { | ||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { | ||||
|         let lookahead = input.lookahead1(); | ||||
|         if lookahead.peek(kw::body) { | ||||
|             let _: kw::body = input.parse()?; | ||||
|             Ok(Self::NewtypeBody) | ||||
|         } else if lookahead.peek(kw::raw_body) { | ||||
|             let _: kw::raw_body = input.parse()?; | ||||
|             Ok(Self::RawBody) | ||||
|         } else if lookahead.peek(kw::header) { | ||||
|             let _: kw::header = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::Header) | ||||
|         } else { | ||||
|             Err(lookahead.error()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(clippy::large_enum_variant)] | ||||
| pub enum DeriveResponseMeta { | ||||
|     ManualBodySerde, | ||||
|     ErrorTy(Type), | ||||
| } | ||||
| 
 | ||||
| impl Parse for DeriveResponseMeta { | ||||
|     fn parse(input: ParseStream<'_>) -> syn::Result<Self> { | ||||
|         let lookahead = input.lookahead1(); | ||||
|         if lookahead.peek(kw::manual_body_serde) { | ||||
|             let _: kw::manual_body_serde = input.parse()?; | ||||
|             Ok(Self::ManualBodySerde) | ||||
|         } else if lookahead.peek(kw::error_ty) { | ||||
|             let _: kw::error_ty = input.parse()?; | ||||
|             let _: Token![=] = input.parse()?; | ||||
|             input.parse().map(Self::ErrorTy) | ||||
|         } else { | ||||
|             Err(lookahead.error()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| use std::{ | ||||
|     collections::{BTreeMap, BTreeSet}, | ||||
|     convert::{TryFrom, TryInto}, | ||||
|     mem, | ||||
| }; | ||||
| 
 | ||||
| use proc_macro2::TokenStream; | ||||
| @ -10,11 +9,11 @@ use syn::{ | ||||
|     parse::{Parse, ParseStream}, | ||||
|     parse_quote, | ||||
|     punctuated::Punctuated, | ||||
|     DeriveInput, Field, Generics, Ident, Lifetime, Lit, LitStr, Token, Type, | ||||
|     DeriveInput, Field, Generics, Ident, Lifetime, LitStr, Token, Type, | ||||
| }; | ||||
| 
 | ||||
| use super::{ | ||||
|     attribute::{Meta, MetaNameValue, MetaValue}, | ||||
|     attribute::{DeriveRequestMeta, RequestMeta}, | ||||
|     auth_scheme::AuthScheme, | ||||
|     util::collect_lifetime_idents, | ||||
| }; | ||||
| @ -62,28 +61,16 @@ pub fn expand_derive_request(input: DeriveInput) -> syn::Result<TokenStream> { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         let meta = attr.parse_args_with(Punctuated::<_, Token![,]>::parse_terminated)?; | ||||
|         for MetaNameValue { name, value } in meta { | ||||
|             match value { | ||||
|                 MetaValue::Type(t) if name == "authentication" => { | ||||
|                     authentication = Some(parse_quote!(#t)); | ||||
|                 } | ||||
|                 MetaValue::Type(t) if name == "method" => { | ||||
|                     method = Some(parse_quote!(#t)); | ||||
|                 } | ||||
|                 MetaValue::Type(t) if name == "error_ty" => { | ||||
|                     error_ty = Some(t); | ||||
|                 } | ||||
|                 MetaValue::Lit(Lit::Str(s)) if name == "unstable" => { | ||||
|                     unstable_path = Some(s); | ||||
|                 } | ||||
|                 MetaValue::Lit(Lit::Str(s)) if name == "r0" => { | ||||
|                     r0_path = Some(s); | ||||
|                 } | ||||
|                 MetaValue::Lit(Lit::Str(s)) if name == "stable" => { | ||||
|                     stable_path = Some(s); | ||||
|                 } | ||||
|                 _ => unreachable!("invalid ruma_api({}) attribute", name), | ||||
|         let metas = | ||||
|             attr.parse_args_with(Punctuated::<DeriveRequestMeta, Token![,]>::parse_terminated)?; | ||||
|         for meta in metas { | ||||
|             match meta { | ||||
|                 DeriveRequestMeta::Authentication(t) => authentication = Some(parse_quote!(#t)), | ||||
|                 DeriveRequestMeta::Method(t) => method = Some(parse_quote!(#t)), | ||||
|                 DeriveRequestMeta::ErrorTy(t) => error_ty = Some(t), | ||||
|                 DeriveRequestMeta::UnstablePath(s) => unstable_path = Some(s), | ||||
|                 DeriveRequestMeta::R0Path(s) => r0_path = Some(s), | ||||
|                 DeriveRequestMeta::StablePath(s) => stable_path = Some(s), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @ -405,17 +392,18 @@ enum RequestField { | ||||
| 
 | ||||
| impl RequestField { | ||||
|     /// Creates a new `RequestField`.
 | ||||
|     fn new(kind: RequestFieldKind, field: Field, header: Option<Ident>) -> Self { | ||||
|         match kind { | ||||
|             RequestFieldKind::Body => RequestField::Body(field), | ||||
|             RequestFieldKind::Header => { | ||||
|                 RequestField::Header(field, header.expect("missing header name")) | ||||
|     fn new(field: Field, kind_attr: Option<RequestMeta>) -> Self { | ||||
|         if let Some(attr) = kind_attr { | ||||
|             match attr { | ||||
|                 RequestMeta::NewtypeBody => RequestField::NewtypeBody(field), | ||||
|                 RequestMeta::RawBody => RequestField::RawBody(field), | ||||
|                 RequestMeta::Path => RequestField::Path(field), | ||||
|                 RequestMeta::Query => RequestField::Query(field), | ||||
|                 RequestMeta::QueryMap => RequestField::QueryMap(field), | ||||
|                 RequestMeta::Header(header) => RequestField::Header(field, header), | ||||
|             } | ||||
|             RequestFieldKind::NewtypeBody => RequestField::NewtypeBody(field), | ||||
|             RequestFieldKind::RawBody => RequestField::RawBody(field), | ||||
|             RequestFieldKind::Path => RequestField::Path(field), | ||||
|             RequestFieldKind::Query => RequestField::Query(field), | ||||
|             RequestFieldKind::QueryMap => RequestField::QueryMap(field), | ||||
|         } else { | ||||
|             RequestField::Body(field) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -477,55 +465,22 @@ impl TryFrom<Field> for RequestField { | ||||
|     type Error = syn::Error; | ||||
| 
 | ||||
|     fn try_from(mut field: Field) -> syn::Result<Self> { | ||||
|         let mut field_kind = None; | ||||
|         let mut header = None; | ||||
|         let (mut api_attrs, attrs) = | ||||
|             field.attrs.into_iter().partition::<Vec<_>, _>(|attr| attr.path.is_ident("ruma_api")); | ||||
|         field.attrs = attrs; | ||||
| 
 | ||||
|         for attr in mem::take(&mut field.attrs) { | ||||
|             let meta = match Meta::from_attribute(&attr)? { | ||||
|                 Some(m) => m, | ||||
|                 None => { | ||||
|                     field.attrs.push(attr); | ||||
|                     continue; | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             if field_kind.is_some() { | ||||
|         let kind_attr = match api_attrs.as_slice() { | ||||
|             [] => None, | ||||
|             [_] => Some(api_attrs.pop().unwrap().parse_args::<RequestMeta>()?), | ||||
|             _ => { | ||||
|                 return Err(syn::Error::new_spanned( | ||||
|                     attr, | ||||
|                     "There can only be one field kind attribute", | ||||
|                     &api_attrs[1], | ||||
|                     "multiple field kind attribute found, there can only be one", | ||||
|                 )); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|             field_kind = Some(match meta { | ||||
|                 Meta::Word(ident) => match &ident.to_string()[..] { | ||||
|                     "body" => RequestFieldKind::NewtypeBody, | ||||
|                     "raw_body" => RequestFieldKind::RawBody, | ||||
|                     "path" => RequestFieldKind::Path, | ||||
|                     "query" => RequestFieldKind::Query, | ||||
|                     "query_map" => RequestFieldKind::QueryMap, | ||||
|                     _ => { | ||||
|                         return Err(syn::Error::new_spanned( | ||||
|                             ident, | ||||
|                             "Invalid #[ruma_api] argument, expected one of \ | ||||
|                             `body`, `raw_body`, `path`, `query`, `query_map`",
 | ||||
|                         )); | ||||
|                     } | ||||
|                 }, | ||||
|                 Meta::NameValue(MetaNameValue { name, value }) => { | ||||
|                     if name != "header" { | ||||
|                         return Err(syn::Error::new_spanned( | ||||
|                             name, | ||||
|                             "Invalid #[ruma_api] argument with value, expected `header`", | ||||
|                         )); | ||||
|                     } | ||||
| 
 | ||||
|                     header = Some(value); | ||||
|                     RequestFieldKind::Header | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         Ok(RequestField::new(field_kind.unwrap_or(RequestFieldKind::Body), field, header)) | ||||
|         Ok(RequestField::new(field, kind_attr)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -540,15 +495,3 @@ impl ToTokens for RequestField { | ||||
|         self.field().to_tokens(tokens) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The types of fields that a request can have, without their values.
 | ||||
| #[derive(Clone, Copy, PartialEq, Eq)] | ||||
| enum RequestFieldKind { | ||||
|     Body, | ||||
|     Header, | ||||
|     NewtypeBody, | ||||
|     RawBody, | ||||
|     Path, | ||||
|     Query, | ||||
|     QueryMap, | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| use std::{ | ||||
|     convert::{TryFrom, TryInto}, | ||||
|     mem, | ||||
|     ops::Not, | ||||
| }; | ||||
| 
 | ||||
| @ -13,7 +12,7 @@ use syn::{ | ||||
|     DeriveInput, Field, Generics, Ident, Lifetime, Token, Type, | ||||
| }; | ||||
| 
 | ||||
| use super::attribute::{Meta, MetaNameValue}; | ||||
| use super::attribute::{DeriveResponseMeta, ResponseMeta}; | ||||
| use crate::util::import_ruma_common; | ||||
| 
 | ||||
| mod incoming; | ||||
| @ -33,18 +32,12 @@ pub fn expand_derive_response(input: DeriveInput) -> syn::Result<TokenStream> { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         let metas = attr.parse_args_with(Punctuated::<Meta<Type>, Token![,]>::parse_terminated)?; | ||||
|         let metas = | ||||
|             attr.parse_args_with(Punctuated::<DeriveResponseMeta, Token![,]>::parse_terminated)?; | ||||
|         for meta in metas { | ||||
|             match meta { | ||||
|                 Meta::Word(w) if w == "manual_body_serde" => { | ||||
|                     manual_body_serde = true; | ||||
|                 } | ||||
|                 Meta::NameValue(MetaNameValue { name, value }) if name == "error_ty" => { | ||||
|                     error_ty = Some(value); | ||||
|                 } | ||||
|                 Meta::Word(name) | Meta::NameValue(MetaNameValue { name, .. }) => { | ||||
|                     unreachable!("invalid ruma_api({}) attribute", name); | ||||
|                 } | ||||
|                 DeriveResponseMeta::ManualBodySerde => manual_body_serde = true, | ||||
|                 DeriveResponseMeta::ErrorTy(t) => error_ty = Some(t), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @ -180,6 +173,19 @@ enum ResponseField { | ||||
| } | ||||
| 
 | ||||
| impl ResponseField { | ||||
|     /// Creates a new `ResponseField`.
 | ||||
|     fn new(field: Field, kind_attr: Option<ResponseMeta>) -> Self { | ||||
|         if let Some(attr) = kind_attr { | ||||
|             match attr { | ||||
|                 ResponseMeta::NewtypeBody => ResponseField::NewtypeBody(field), | ||||
|                 ResponseMeta::RawBody => ResponseField::RawBody(field), | ||||
|                 ResponseMeta::Header(header) => ResponseField::Header(field, header), | ||||
|             } | ||||
|         } else { | ||||
|             ResponseField::Body(field) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Gets the inner `Field` value.
 | ||||
|     fn field(&self) -> &Field { | ||||
|         match self { | ||||
| @ -226,58 +232,22 @@ impl TryFrom<Field> for ResponseField { | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|         let mut field_kind = None; | ||||
|         let mut header = None; | ||||
|         let (mut api_attrs, attrs) = | ||||
|             field.attrs.into_iter().partition::<Vec<_>, _>(|attr| attr.path.is_ident("ruma_api")); | ||||
|         field.attrs = attrs; | ||||
| 
 | ||||
|         for attr in mem::take(&mut field.attrs) { | ||||
|             let meta = match Meta::from_attribute(&attr)? { | ||||
|                 Some(m) => m, | ||||
|                 None => { | ||||
|                     field.attrs.push(attr); | ||||
|                     continue; | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             if field_kind.is_some() { | ||||
|         let kind_attr = match api_attrs.as_slice() { | ||||
|             [] => None, | ||||
|             [_] => Some(api_attrs.pop().unwrap().parse_args::<ResponseMeta>()?), | ||||
|             _ => { | ||||
|                 return Err(syn::Error::new_spanned( | ||||
|                     attr, | ||||
|                     "There can only be one field kind attribute", | ||||
|                     &api_attrs[1], | ||||
|                     "multiple field kind attribute found, there can only be one", | ||||
|                 )); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|             field_kind = Some(match meta { | ||||
|                 Meta::Word(ident) => match &ident.to_string()[..] { | ||||
|                     "body" => ResponseFieldKind::NewtypeBody, | ||||
|                     "raw_body" => ResponseFieldKind::RawBody, | ||||
|                     _ => { | ||||
|                         return Err(syn::Error::new_spanned( | ||||
|                             ident, | ||||
|                             "Invalid #[ruma_api] argument with value, expected `body`", | ||||
|                         )); | ||||
|                     } | ||||
|                 }, | ||||
|                 Meta::NameValue(MetaNameValue { name, value }) => { | ||||
|                     if name != "header" { | ||||
|                         return Err(syn::Error::new_spanned( | ||||
|                             name, | ||||
|                             "Invalid #[ruma_api] argument with value, expected `header`", | ||||
|                         )); | ||||
|                     } | ||||
| 
 | ||||
|                     header = Some(value); | ||||
|                     ResponseFieldKind::Header | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         Ok(match field_kind.unwrap_or(ResponseFieldKind::Body) { | ||||
|             ResponseFieldKind::Body => ResponseField::Body(field), | ||||
|             ResponseFieldKind::Header => { | ||||
|                 ResponseField::Header(field, header.expect("missing header name")) | ||||
|             } | ||||
|             ResponseFieldKind::NewtypeBody => ResponseField::NewtypeBody(field), | ||||
|             ResponseFieldKind::RawBody => ResponseField::RawBody(field), | ||||
|         }) | ||||
|         Ok(ResponseField::new(field, kind_attr)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -293,14 +263,6 @@ impl ToTokens for ResponseField { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// The types of fields that a response can have, without their values.
 | ||||
| enum ResponseFieldKind { | ||||
|     Body, | ||||
|     Header, | ||||
|     NewtypeBody, | ||||
|     RawBody, | ||||
| } | ||||
| 
 | ||||
| fn has_lifetime(ty: &Type) -> bool { | ||||
|     struct Visitor { | ||||
|         found_lifetime: bool, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user