| 1 | use crate::attributes::{CrateAttribute, ExprPathWrap, RenamingRule}; |
| 2 | use proc_macro2::{Span, TokenStream}; |
| 3 | use quote::{quote, quote_spanned, ToTokens}; |
| 4 | use std::ffi::CString; |
| 5 | use syn::spanned::Spanned; |
| 6 | use syn::{punctuated::Punctuated, Token}; |
| 7 | |
| 8 | /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. |
| 9 | macro_rules! err_spanned { |
| 10 | ($span:expr => $msg:expr) => { |
| 11 | syn::Error::new($span, $msg) |
| 12 | }; |
| 13 | } |
| 14 | |
| 15 | /// Macro inspired by `anyhow::bail!` to return a compiler error with the given span. |
| 16 | macro_rules! bail_spanned { |
| 17 | ($span:expr => $msg:expr) => { |
| 18 | return Err(err_spanned!($span => $msg)) |
| 19 | }; |
| 20 | } |
| 21 | |
| 22 | /// Macro inspired by `anyhow::ensure!` to return a compiler error with the given span if the |
| 23 | /// specified condition is not met. |
| 24 | macro_rules! ensure_spanned { |
| 25 | ($condition:expr, $span:expr => $msg:expr) => { |
| 26 | if !($condition) { |
| 27 | bail_spanned!($span => $msg); |
| 28 | } |
| 29 | }; |
| 30 | ($($condition:expr, $span:expr => $msg:expr;)*) => { |
| 31 | if let Some(e) = [$( |
| 32 | (!($condition)).then(|| err_spanned!($span => $msg)), |
| 33 | )*] |
| 34 | .into_iter() |
| 35 | .flatten() |
| 36 | .reduce(|mut acc, e| { |
| 37 | acc.combine(e); |
| 38 | acc |
| 39 | }) { |
| 40 | return Err(e); |
| 41 | } |
| 42 | }; |
| 43 | } |
| 44 | |
| 45 | /// Check if the given type `ty` is `pyo3::Python`. |
| 46 | pub fn is_python(ty: &syn::Type) -> bool { |
| 47 | match unwrap_ty_group(ty) { |
| 48 | syn::Type::Path(typath: &TypePath) => typath |
| 49 | .path |
| 50 | .segments |
| 51 | .last() |
| 52 | .map(|seg| seg.ident == "Python" ) |
| 53 | .unwrap_or(default:false), |
| 54 | _ => false, |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /// If `ty` is `Option<T>`, return `Some(T)`, else `None`. |
| 59 | pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> { |
| 60 | if let syn::Type::Path(syn::TypePath { path: &Path, .. }) = ty { |
| 61 | let seg: &PathSegment = path.segments.last().filter(|s: &&PathSegment| s.ident == "Option" )?; |
| 62 | if let syn::PathArguments::AngleBracketed(params: &AngleBracketedGenericArguments) = &seg.arguments { |
| 63 | if let syn::GenericArgument::Type(ty: &Type) = params.args.first()? { |
| 64 | return Some(ty); |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | None |
| 69 | } |
| 70 | |
| 71 | // TODO: Replace usage of this by [`syn::LitCStr`] when on MSRV 1.77 |
| 72 | #[derive (Clone)] |
| 73 | pub struct LitCStr { |
| 74 | lit: CString, |
| 75 | span: Span, |
| 76 | pyo3_path: PyO3CratePath, |
| 77 | } |
| 78 | |
| 79 | impl LitCStr { |
| 80 | pub fn new(lit: CString, span: Span, ctx: &Ctx) -> Self { |
| 81 | Self { |
| 82 | lit, |
| 83 | span, |
| 84 | pyo3_path: ctx.pyo3_path.clone(), |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | pub fn empty(ctx: &Ctx) -> Self { |
| 89 | Self { |
| 90 | lit: CString::new("" ).unwrap(), |
| 91 | span: Span::call_site(), |
| 92 | pyo3_path: ctx.pyo3_path.clone(), |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | impl quote::ToTokens for LitCStr { |
| 98 | fn to_tokens(&self, tokens: &mut TokenStream) { |
| 99 | if cfg!(c_str_lit) { |
| 100 | syn::LitCStr::new(&self.lit, self.span).to_tokens(tokens); |
| 101 | } else { |
| 102 | let pyo3_path: &PyO3CratePath = &self.pyo3_path; |
| 103 | let lit: &str = self.lit.to_str().unwrap(); |
| 104 | tokens.extend(iter:quote::quote_spanned!(self.span => #pyo3_path::ffi::c_str!(#lit))); |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | /// A syntax tree which evaluates to a nul-terminated docstring for Python. |
| 110 | /// |
| 111 | /// Typically the tokens will just be that string, but if the original docs included macro |
| 112 | /// expressions then the tokens will be a concat!("...", "\n", "\0") expression of the strings and |
| 113 | /// macro parts. contents such as parse the string contents. |
| 114 | #[derive (Clone)] |
| 115 | pub struct PythonDoc(PythonDocKind); |
| 116 | |
| 117 | #[derive (Clone)] |
| 118 | enum PythonDocKind { |
| 119 | LitCStr(LitCStr), |
| 120 | // There is currently no way to `concat!` c-string literals, we fallback to the `c_str!` macro in |
| 121 | // this case. |
| 122 | Tokens(TokenStream), |
| 123 | } |
| 124 | |
| 125 | /// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string. |
| 126 | /// |
| 127 | /// If this doc is for a callable, the provided `text_signature` can be passed to prepend |
| 128 | /// this to the documentation suitable for Python to extract this into the `__text_signature__` |
| 129 | /// attribute. |
| 130 | pub fn get_doc( |
| 131 | attrs: &[syn::Attribute], |
| 132 | mut text_signature: Option<String>, |
| 133 | ctx: &Ctx, |
| 134 | ) -> PythonDoc { |
| 135 | let Ctx { pyo3_path, .. } = ctx; |
| 136 | // insert special divider between `__text_signature__` and doc |
| 137 | // (assume text_signature is itself well-formed) |
| 138 | if let Some(text_signature) = &mut text_signature { |
| 139 | text_signature.push_str(" \n-- \n\n" ); |
| 140 | } |
| 141 | |
| 142 | let mut parts = Punctuated::<TokenStream, Token![,]>::new(); |
| 143 | let mut first = true; |
| 144 | let mut current_part = text_signature.unwrap_or_default(); |
| 145 | |
| 146 | for attr in attrs { |
| 147 | if attr.path().is_ident("doc" ) { |
| 148 | if let Ok(nv) = attr.meta.require_name_value() { |
| 149 | if !first { |
| 150 | current_part.push(' \n' ); |
| 151 | } else { |
| 152 | first = false; |
| 153 | } |
| 154 | if let syn::Expr::Lit(syn::ExprLit { |
| 155 | lit: syn::Lit::Str(lit_str), |
| 156 | .. |
| 157 | }) = &nv.value |
| 158 | { |
| 159 | // Strip single left space from literal strings, if needed. |
| 160 | // e.g. `/// Hello world` expands to #[doc = " Hello world"] |
| 161 | let doc_line = lit_str.value(); |
| 162 | current_part.push_str(doc_line.strip_prefix(' ' ).unwrap_or(&doc_line)); |
| 163 | } else { |
| 164 | // This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)] |
| 165 | // Reset the string buffer, write that part, and then push this macro part too. |
| 166 | parts.push(current_part.to_token_stream()); |
| 167 | current_part.clear(); |
| 168 | parts.push(nv.value.to_token_stream()); |
| 169 | } |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | if !parts.is_empty() { |
| 175 | // Doc contained macro pieces - return as `concat!` expression |
| 176 | if !current_part.is_empty() { |
| 177 | parts.push(current_part.to_token_stream()); |
| 178 | } |
| 179 | |
| 180 | let mut tokens = TokenStream::new(); |
| 181 | |
| 182 | syn::Ident::new("concat" , Span::call_site()).to_tokens(&mut tokens); |
| 183 | syn::token::Not(Span::call_site()).to_tokens(&mut tokens); |
| 184 | syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| { |
| 185 | parts.to_tokens(tokens); |
| 186 | syn::token::Comma(Span::call_site()).to_tokens(tokens); |
| 187 | }); |
| 188 | |
| 189 | PythonDoc(PythonDocKind::Tokens( |
| 190 | quote!(#pyo3_path::ffi::c_str!(#tokens)), |
| 191 | )) |
| 192 | } else { |
| 193 | // Just a string doc - return directly with nul terminator |
| 194 | let docs = CString::new(current_part).unwrap(); |
| 195 | PythonDoc(PythonDocKind::LitCStr(LitCStr::new( |
| 196 | docs, |
| 197 | Span::call_site(), |
| 198 | ctx, |
| 199 | ))) |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | impl quote::ToTokens for PythonDoc { |
| 204 | fn to_tokens(&self, tokens: &mut TokenStream) { |
| 205 | match &self.0 { |
| 206 | PythonDocKind::LitCStr(lit: &LitCStr) => lit.to_tokens(tokens), |
| 207 | PythonDocKind::Tokens(toks: &TokenStream) => toks.to_tokens(tokens), |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { |
| 213 | while let syn::Type::Group(g: &TypeGroup) = ty { |
| 214 | ty = &*g.elem; |
| 215 | } |
| 216 | ty |
| 217 | } |
| 218 | |
| 219 | pub struct Ctx { |
| 220 | /// Where we can find the pyo3 crate |
| 221 | pub pyo3_path: PyO3CratePath, |
| 222 | |
| 223 | /// If we are in a pymethod or pyfunction, |
| 224 | /// this will be the span of the return type |
| 225 | pub output_span: Span, |
| 226 | } |
| 227 | |
| 228 | impl Ctx { |
| 229 | pub(crate) fn new(attr: &Option<CrateAttribute>, signature: Option<&syn::Signature>) -> Self { |
| 230 | let pyo3_path: PyO3CratePath = match attr { |
| 231 | Some(attr: &KeywordAttribute>) => PyO3CratePath::Given(attr.value.0.clone()), |
| 232 | None => PyO3CratePath::Default, |
| 233 | }; |
| 234 | |
| 235 | let output_span: Span = if let Some(syn::Signature { |
| 236 | output: syn::ReturnType::Type(_, output_type: &Box), |
| 237 | .. |
| 238 | }) = &signature |
| 239 | { |
| 240 | output_type.span() |
| 241 | } else { |
| 242 | Span::call_site() |
| 243 | }; |
| 244 | |
| 245 | Self { |
| 246 | pyo3_path, |
| 247 | output_span, |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | #[derive (Clone)] |
| 253 | pub enum PyO3CratePath { |
| 254 | Given(syn::Path), |
| 255 | Default, |
| 256 | } |
| 257 | |
| 258 | impl PyO3CratePath { |
| 259 | pub fn to_tokens_spanned(&self, span: Span) -> TokenStream { |
| 260 | match self { |
| 261 | Self::Given(path: &Path) => quote::quote_spanned! { span => #path }, |
| 262 | Self::Default => quote::quote_spanned! { span => ::pyo3 }, |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | impl quote::ToTokens for PyO3CratePath { |
| 268 | fn to_tokens(&self, tokens: &mut TokenStream) { |
| 269 | match self { |
| 270 | Self::Given(path: &Path) => path.to_tokens(tokens), |
| 271 | Self::Default => quote::quote! { ::pyo3 }.to_tokens(tokens), |
| 272 | } |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String { |
| 277 | use heck::*; |
| 278 | |
| 279 | match rule { |
| 280 | RenamingRule::CamelCase => name.to_lower_camel_case(), |
| 281 | RenamingRule::KebabCase => name.to_kebab_case(), |
| 282 | RenamingRule::Lowercase => name.to_lowercase(), |
| 283 | RenamingRule::PascalCase => name.to_upper_camel_case(), |
| 284 | RenamingRule::ScreamingKebabCase => name.to_shouty_kebab_case(), |
| 285 | RenamingRule::ScreamingSnakeCase => name.to_shouty_snake_case(), |
| 286 | RenamingRule::SnakeCase => name.to_snake_case(), |
| 287 | RenamingRule::Uppercase => name.to_uppercase(), |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | pub(crate) enum IdentOrStr<'a> { |
| 292 | Str(&'a str), |
| 293 | Ident(syn::Ident), |
| 294 | } |
| 295 | |
| 296 | pub(crate) fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool { |
| 297 | has_attribute_with_namespace(attrs, crate_path:None, &[ident]) |
| 298 | } |
| 299 | |
| 300 | pub(crate) fn has_attribute_with_namespace( |
| 301 | attrs: &[syn::Attribute], |
| 302 | crate_path: Option<&PyO3CratePath>, |
| 303 | idents: &[&str], |
| 304 | ) -> bool { |
| 305 | let mut segments: Vec> = vec![]; |
| 306 | if let Some(c: &PyO3CratePath) = crate_path { |
| 307 | match c { |
| 308 | PyO3CratePath::Given(paths: &Path) => { |
| 309 | for p: &PathSegment in &paths.segments { |
| 310 | segments.push(IdentOrStr::Ident(p.ident.clone())); |
| 311 | } |
| 312 | } |
| 313 | PyO3CratePath::Default => segments.push(IdentOrStr::Str("pyo3" )), |
| 314 | } |
| 315 | }; |
| 316 | for i: &&str in idents { |
| 317 | segments.push(IdentOrStr::Str(i)); |
| 318 | } |
| 319 | |
| 320 | attrs.iter().any(|attr: &Attribute| { |
| 321 | segmentsIter<'_, IdentOrStr<'_>> |
| 322 | .iter() |
| 323 | .eq(attr.path().segments.iter().map(|v: &PathSegment| &v.ident)) |
| 324 | }) |
| 325 | } |
| 326 | |
| 327 | pub(crate) fn deprecated_from_py_with(expr_path: &ExprPathWrap) -> Option<TokenStream> { |
| 328 | let path: String = quote!(#expr_path).to_string(); |
| 329 | let msg: String = |
| 330 | format!("remove the quotes from the literal \n= help: use ` {path}` instead of ` \"{path}\"`" ); |
| 331 | expr_path.from_lit_str.then(|| { |
| 332 | quote_spanned! { expr_path.span() => |
| 333 | #[deprecated(since = "0.24.0" , note = #msg)] |
| 334 | #[allow(dead_code)] |
| 335 | const LIT_STR_DEPRECATION: () = (); |
| 336 | let _: () = LIT_STR_DEPRECATION; |
| 337 | } |
| 338 | }) |
| 339 | } |
| 340 | |
| 341 | pub(crate) trait TypeExt { |
| 342 | /// Replaces all explicit lifetimes in `self` with elided (`'_`) lifetimes |
| 343 | /// |
| 344 | /// This is useful if `Self` is used in `const` context, where explicit |
| 345 | /// lifetimes are not allowed (yet). |
| 346 | fn elide_lifetimes(self) -> Self; |
| 347 | } |
| 348 | |
| 349 | impl TypeExt for syn::Type { |
| 350 | fn elide_lifetimes(mut self) -> Self { |
| 351 | fn elide_lifetimes(ty: &mut syn::Type) { |
| 352 | match ty { |
| 353 | syn::Type::Path(type_path) => { |
| 354 | if let Some(qself) = &mut type_path.qself { |
| 355 | elide_lifetimes(&mut qself.ty) |
| 356 | } |
| 357 | for seg in &mut type_path.path.segments { |
| 358 | if let syn::PathArguments::AngleBracketed(args) = &mut seg.arguments { |
| 359 | for generic_arg in &mut args.args { |
| 360 | match generic_arg { |
| 361 | syn::GenericArgument::Lifetime(lt) => { |
| 362 | *lt = syn::Lifetime::new("'_" , lt.span()); |
| 363 | } |
| 364 | syn::GenericArgument::Type(ty) => elide_lifetimes(ty), |
| 365 | syn::GenericArgument::AssocType(assoc) => { |
| 366 | elide_lifetimes(&mut assoc.ty) |
| 367 | } |
| 368 | |
| 369 | syn::GenericArgument::Const(_) |
| 370 | | syn::GenericArgument::AssocConst(_) |
| 371 | | syn::GenericArgument::Constraint(_) |
| 372 | | _ => {} |
| 373 | } |
| 374 | } |
| 375 | } |
| 376 | } |
| 377 | } |
| 378 | syn::Type::Reference(type_ref) => { |
| 379 | if let Some(lt) = type_ref.lifetime.as_mut() { |
| 380 | *lt = syn::Lifetime::new("'_" , lt.span()); |
| 381 | } |
| 382 | elide_lifetimes(&mut type_ref.elem); |
| 383 | } |
| 384 | syn::Type::Tuple(type_tuple) => { |
| 385 | for ty in &mut type_tuple.elems { |
| 386 | elide_lifetimes(ty); |
| 387 | } |
| 388 | } |
| 389 | syn::Type::Array(type_array) => elide_lifetimes(&mut type_array.elem), |
| 390 | syn::Type::Slice(ty) => elide_lifetimes(&mut ty.elem), |
| 391 | syn::Type::Group(ty) => elide_lifetimes(&mut ty.elem), |
| 392 | syn::Type::Paren(ty) => elide_lifetimes(&mut ty.elem), |
| 393 | syn::Type::Ptr(ty) => elide_lifetimes(&mut ty.elem), |
| 394 | |
| 395 | syn::Type::BareFn(_) |
| 396 | | syn::Type::ImplTrait(_) |
| 397 | | syn::Type::Infer(_) |
| 398 | | syn::Type::Macro(_) |
| 399 | | syn::Type::Never(_) |
| 400 | | syn::Type::TraitObject(_) |
| 401 | | syn::Type::Verbatim(_) |
| 402 | | _ => {} |
| 403 | } |
| 404 | } |
| 405 | |
| 406 | elide_lifetimes(&mut self); |
| 407 | self |
| 408 | } |
| 409 | } |
| 410 | |