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 | |