pyo3_stub_gen/docgen/
builder.rs

1//! Builder for converting StubInfo to DocPackage
2
3use crate::docgen::{
4    export::ExportResolver,
5    ir::{
6        DeprecatedInfo, DocAttribute, DocClass, DocFunction, DocItem, DocModule, DocPackage,
7        DocParameter, DocSignature, DocSubmodule, DocTypeAlias, DocTypeExpr, DocVariable,
8    },
9    types::TypeRenderer,
10    util::{is_hidden_module, prefix_stripper},
11};
12use crate::generate::StubInfo;
13use crate::Result;
14use std::collections::BTreeMap;
15use std::path::PathBuf;
16
17/// Helper to check if item already exists in the list
18fn matches_item_name(item: &DocItem, name: &str) -> bool {
19    match item {
20        DocItem::Function(f) => f.name == name,
21        DocItem::Class(c) => c.name == name,
22        DocItem::TypeAlias(t) => t.name == name,
23        DocItem::Variable(v) => v.name == name,
24        DocItem::Module(m) => m.name == name,
25    }
26}
27
28/// Context for building documentation items, containing shared rendering components
29struct DocBuildContext<'a> {
30    link_resolver: crate::docgen::link::LinkResolver<'a>,
31    module: &'a str,
32}
33
34impl<'a> DocBuildContext<'a> {
35    /// Get a type renderer for this context
36    fn type_renderer(&self) -> TypeRenderer<'_> {
37        TypeRenderer::new(&self.link_resolver, self.module)
38    }
39
40    /// Get a default value parser for this context
41    fn default_parser(&self) -> crate::docgen::default_parser::DefaultValueParser<'_> {
42        crate::docgen::default_parser::DefaultValueParser::new(&self.link_resolver, self.module)
43    }
44}
45
46/// Builder for converting StubInfo to DocPackage
47pub struct DocPackageBuilder<'a> {
48    stub_info: &'a StubInfo,
49    export_resolver: ExportResolver<'a>,
50    export_map: BTreeMap<String, String>,
51}
52
53impl<'a> DocPackageBuilder<'a> {
54    pub fn new(stub_info: &'a StubInfo) -> Self {
55        let export_resolver = ExportResolver::new(&stub_info.modules);
56        let export_map = export_resolver.build_export_map();
57
58        Self {
59            stub_info,
60            export_resolver,
61            export_map,
62        }
63    }
64
65    /// Create a build context for a specific module
66    fn create_context<'b>(&'b self, module: &'b str) -> DocBuildContext<'b> {
67        let link_resolver = crate::docgen::link::LinkResolver::new(&self.export_map);
68        DocBuildContext {
69            link_resolver,
70            module,
71        }
72    }
73
74    pub fn build(self) -> Result<DocPackage> {
75        let mut modules = BTreeMap::new();
76
77        for (module_name, module) in &self.stub_info.modules {
78            // Skip modules with any component starting with '_'
79            if is_hidden_module(module_name) {
80                continue;
81            }
82
83            let doc_module = self.build_module(module_name, module)?;
84            modules.insert(module_name.clone(), doc_module);
85        }
86
87        let export_map = self.export_map;
88
89        // Convert config to use relative POSIX path for JSON serialization
90        let mut json_config = self.stub_info.config.doc_gen.clone().unwrap_or_default();
91        if let Some(pyproject_dir) = &self.stub_info.pyproject_dir {
92            let relative_posix = json_config.to_relative_posix_path(pyproject_dir);
93            json_config.output_dir = PathBuf::from(relative_posix);
94        }
95
96        Ok(DocPackage {
97            name: self.stub_info.project_name.clone(),
98            modules,
99            export_map,
100            config: json_config,
101        })
102    }
103
104    fn build_module(&self, name: &str, module: &crate::generate::Module) -> Result<DocModule> {
105        let exports = self.export_resolver.resolve_exports(module);
106        let mut items = Vec::new();
107
108        // Process functions - handle overloads (Requirement #1)
109        for (func_name, func_defs) in &module.function {
110            if exports.contains(*func_name) {
111                items.push(self.build_function(name, func_defs)?);
112            }
113        }
114
115        // Process type aliases (Requirement #2)
116        for (alias_name, alias_def) in &module.type_aliases {
117            if exports.contains(*alias_name) {
118                items.push(self.build_type_alias(name, alias_def)?);
119            }
120        }
121
122        // Process classes (sorted by name for deterministic output)
123        let mut classes: Vec<_> = module.class.values().collect();
124        classes.sort_by_key(|c| c.name);
125        for class_def in classes {
126            if exports.contains(class_def.name) {
127                items.push(self.build_class(name, class_def)?);
128            }
129        }
130
131        // Process enums (sorted by name for deterministic output)
132        let mut enums: Vec<_> = module.enum_.values().collect();
133        enums.sort_by_key(|e| e.name);
134        for enum_def in enums {
135            if exports.contains(enum_def.name) {
136                items.push(self.build_enum_as_class(name, enum_def)?);
137            }
138        }
139
140        // Process variables
141        for (var_name, var_def) in &module.variables {
142            if exports.contains(*var_name) {
143                items.push(self.build_variable(name, var_def)?);
144            }
145        }
146
147        // Process module re-exports (from reexport_module_members!)
148        for re_export in &module.module_re_exports {
149            if let Some(source_module) = self.stub_info.modules.get(&re_export.source_module) {
150                for item_name in &re_export.items {
151                    // Skip if already added (prefer directly-defined items)
152                    if items.iter().any(|item| matches_item_name(item, item_name)) {
153                        continue;
154                    }
155
156                    // Build re-exported item (link targets will be corrected later)
157                    if let Some(item) = self.build_reexported_item(
158                        &re_export.source_module,
159                        source_module,
160                        item_name,
161                    )? {
162                        items.push(item);
163                    }
164                }
165            }
166        }
167
168        // Process submodules - convert to DocItem::Module entries
169        for submod_name in &module.submodules {
170            if !submod_name.starts_with('_') && exports.contains(submod_name) {
171                // Construct FQN for the submodule
172                let submod_fqn = if name.is_empty() {
173                    submod_name.clone()
174                } else {
175                    format!("{}.{}", name, submod_name)
176                };
177
178                // Retrieve the submodule's doc from stub_info.modules
179                let submod_doc = self
180                    .stub_info
181                    .modules
182                    .get(&submod_fqn)
183                    .map(|m| m.doc.clone())
184                    .unwrap_or_default();
185
186                items.push(DocItem::Module(DocSubmodule {
187                    name: submod_name.clone(),
188                    doc: submod_doc,
189                    fqn: submod_fqn,
190                }));
191            }
192        }
193
194        // Correct all link targets and display text in all items
195        // This ensures both directly-defined and re-exported items have correct references
196        for item in &mut items {
197            self.correct_link_targets(item, name);
198        }
199
200        Ok(DocModule {
201            name: name.to_string(),
202            doc: module.doc.clone(),
203            items,
204            submodules: module
205                .submodules
206                .iter()
207                .filter(|s| !s.starts_with('_'))
208                .cloned()
209                .collect(),
210        })
211    }
212
213    fn build_function(
214        &self,
215        module: &str,
216        func_defs: &[crate::generate::FunctionDef],
217    ) -> Result<DocItem> {
218        // Sort overloads by source location for deterministic ordering (same as stub generation)
219        let mut sorted_defs = func_defs.to_vec();
220        sorted_defs.sort_by_key(|func| (func.file, func.line, func.column, func.index));
221
222        // Requirement #1: Include ALL overload signatures
223        let signatures: Vec<DocSignature> = sorted_defs
224            .iter()
225            .map(|def| self.build_signature(module, def))
226            .collect::<Result<_>>()?;
227
228        // Use first def's doc (they should all have same doc)
229        let doc = sorted_defs
230            .first()
231            .map(|d| d.doc.to_string())
232            .unwrap_or_default();
233
234        let deprecated = sorted_defs.first().and_then(|d| {
235            d.deprecated.as_ref().map(|dep| DeprecatedInfo {
236                since: dep.since.map(|s| s.to_string()),
237                note: dep.note.map(|s| s.to_string()),
238            })
239        });
240
241        Ok(DocItem::Function(DocFunction {
242            name: sorted_defs[0].name.to_string(),
243            doc,
244            signatures,
245            is_async: sorted_defs[0].is_async,
246            deprecated,
247        }))
248    }
249
250    fn build_signature_from_params(
251        &self,
252        module: &str,
253        parameters: &crate::generate::Parameters,
254        return_type: &crate::TypeInfo,
255    ) -> Result<DocSignature> {
256        let ctx = self.create_context(module);
257        let type_renderer = ctx.type_renderer();
258        let default_parser = ctx.default_parser();
259
260        let params: Vec<DocParameter> = parameters
261            .positional_only
262            .iter()
263            .chain(parameters.positional_or_keyword.iter())
264            .chain(parameters.keyword_only.iter())
265            .chain(parameters.varargs.iter())
266            .chain(parameters.varkw.iter())
267            .map(|param| DocParameter {
268                name: param.name.to_string(),
269                type_: type_renderer.render_type(&param.type_info),
270                default: match &param.default {
271                    crate::generate::ParameterDefault::None => None,
272                    crate::generate::ParameterDefault::Expr { value, .. } => {
273                        // Parse default value and identify type references
274                        Some(default_parser.parse(value, &param.type_info))
275                    }
276                },
277            })
278            .collect();
279
280        let ret_type = Some(type_renderer.render_type(return_type));
281
282        Ok(DocSignature {
283            parameters: params,
284            return_type: ret_type,
285        })
286    }
287
288    fn build_signature(
289        &self,
290        module: &str,
291        def: &crate::generate::FunctionDef,
292    ) -> Result<DocSignature> {
293        self.build_signature_from_params(module, &def.parameters, &def.r#return)
294    }
295
296    fn build_signature_from_method(
297        &self,
298        module: &str,
299        def: &crate::generate::MethodDef,
300    ) -> Result<DocSignature> {
301        self.build_signature_from_params(module, &def.parameters, &def.r#return)
302    }
303
304    fn build_type_alias(
305        &self,
306        module: &str,
307        alias: &crate::generate::TypeAliasDef,
308    ) -> Result<DocItem> {
309        // Requirement #2: Preserve alias definition + docstring
310        let ctx = self.create_context(module);
311        let type_renderer = ctx.type_renderer();
312
313        Ok(DocItem::TypeAlias(DocTypeAlias {
314            name: alias.name.to_string(),
315            doc: alias.doc.to_string(),
316            definition: type_renderer.render_type(&alias.type_),
317        }))
318    }
319
320    fn build_class(&self, module: &str, class: &crate::generate::ClassDef) -> Result<DocItem> {
321        let ctx = self.create_context(module);
322        let type_renderer = ctx.type_renderer();
323
324        let bases: Vec<DocTypeExpr> = class
325            .bases
326            .iter()
327            .map(|base| type_renderer.render_type(base))
328            .collect();
329
330        let mut methods = Vec::new();
331        for (method_name, method_overloads) in &class.methods {
332            let signatures: Vec<DocSignature> = method_overloads
333                .iter()
334                .map(|method| self.build_signature_from_method(module, method))
335                .collect::<Result<_>>()?;
336
337            let deprecated = method_overloads.first().and_then(|d| {
338                d.deprecated.as_ref().map(|dep| DeprecatedInfo {
339                    since: dep.since.map(|s| s.to_string()),
340                    note: dep.note.map(|s| s.to_string()),
341                })
342            });
343
344            methods.push(DocFunction {
345                name: method_name.to_string(),
346                doc: method_overloads
347                    .first()
348                    .map(|m| m.doc.to_string())
349                    .unwrap_or_default(),
350                signatures,
351                is_async: method_overloads
352                    .first()
353                    .map(|m| m.is_async)
354                    .unwrap_or(false),
355                deprecated,
356            });
357        }
358
359        // Convert plain #[pyo3(get, set)] field attributes
360        let mut attributes: Vec<DocAttribute> = class
361            .attrs
362            .iter()
363            .map(|attr| DocAttribute {
364                name: attr.name.to_string(),
365                doc: attr.doc.to_string(),
366                type_: Some(type_renderer.render_type(&attr.r#type)),
367                is_property: false,
368                is_readonly: false,
369                deprecated: attr.deprecated.as_ref().map(|dep| DeprecatedInfo {
370                    since: dep.since.map(|s| s.to_string()),
371                    note: dep.note.map(|s| s.to_string()),
372                }),
373            })
374            .collect();
375
376        // Convert #[getter]/#[setter] method properties
377        for (prop_name, (getter, setter)) in &class.getter_setters {
378            let member = getter.as_ref().or(setter.as_ref());
379            if let Some(member) = member {
380                // Use deprecation from getter preferentially, fall back to setter
381                let deprecated = getter
382                    .as_ref()
383                    .and_then(|g| g.deprecated.as_ref())
384                    .or_else(|| setter.as_ref().and_then(|s| s.deprecated.as_ref()))
385                    .map(|dep| DeprecatedInfo {
386                        since: dep.since.map(|s| s.to_string()),
387                        note: dep.note.map(|s| s.to_string()),
388                    });
389                attributes.push(DocAttribute {
390                    name: prop_name.clone(),
391                    doc: member.doc.to_string(),
392                    type_: Some(type_renderer.render_type(&member.r#type)),
393                    is_property: true,
394                    is_readonly: setter.is_none(),
395                    deprecated,
396                });
397            }
398        }
399
400        Ok(DocItem::Class(DocClass {
401            name: class.name.to_string(),
402            doc: class.doc.to_string(),
403            bases,
404            methods,
405            attributes,
406            deprecated: None, // ClassDef doesn't have deprecated field
407        }))
408    }
409
410    fn build_enum_as_class(
411        &self,
412        module: &str,
413        enum_def: &crate::generate::EnumDef,
414    ) -> Result<DocItem> {
415        let ctx = self.create_context(module);
416        let type_renderer = ctx.type_renderer();
417
418        // Convert enum variants to DocAttributes
419        let mut attributes: Vec<DocAttribute> = enum_def
420            .variants
421            .iter()
422            .map(|(variant_name, variant_doc)| DocAttribute {
423                name: (*variant_name).to_string(),
424                doc: (*variant_doc).to_string(),
425                type_: None, // Enum variants don't have explicit type annotations
426                is_property: false,
427                is_readonly: false,
428                deprecated: None,
429            })
430            .collect();
431
432        // Collect setter names for determining readonly status
433        let setter_names: std::collections::HashSet<&str> =
434            enum_def.setters.iter().map(|s| s.name).collect();
435
436        // Convert enum getters to property attributes
437        for getter in &enum_def.getters {
438            attributes.push(DocAttribute {
439                name: getter.name.to_string(),
440                doc: getter.doc.to_string(),
441                type_: Some(type_renderer.render_type(&getter.r#type)),
442                is_property: true,
443                is_readonly: !setter_names.contains(getter.name),
444                deprecated: getter.deprecated.as_ref().map(|dep| DeprecatedInfo {
445                    since: dep.since.map(|s| s.to_string()),
446                    note: dep.note.map(|s| s.to_string()),
447                }),
448            });
449        }
450
451        Ok(DocItem::Class(DocClass {
452            name: enum_def.name.to_string(),
453            doc: enum_def.doc.to_string(),
454            bases: Vec::new(), // Enums don't have bases in our structure
455            methods: Vec::new(),
456            attributes,
457            deprecated: None,
458        }))
459    }
460
461    fn build_variable(&self, module: &str, var: &crate::generate::VariableDef) -> Result<DocItem> {
462        let ctx = self.create_context(module);
463        let type_renderer = ctx.type_renderer();
464
465        Ok(DocItem::Variable(DocVariable {
466            name: var.name.to_string(),
467            doc: String::new(), // VariableDef doesn't have doc field
468            type_: Some(type_renderer.render_type(&var.type_)),
469        }))
470    }
471
472    /// Build a re-exported item from a source module
473    fn build_reexported_item(
474        &self,
475        source_module_name: &str,
476        source_module: &crate::generate::Module,
477        item_name: &str,
478    ) -> Result<Option<DocItem>> {
479        // Try functions
480        if let Some(func_defs) = source_module.function.get(item_name) {
481            return Ok(Some(self.build_function(source_module_name, func_defs)?));
482        }
483
484        // Try classes
485        for class_def in source_module.class.values() {
486            if class_def.name == item_name {
487                return Ok(Some(self.build_class(source_module_name, class_def)?));
488            }
489        }
490
491        // Try enums
492        for enum_def in source_module.enum_.values() {
493            if enum_def.name == item_name {
494                return Ok(Some(
495                    self.build_enum_as_class(source_module_name, enum_def)?,
496                ));
497            }
498        }
499
500        // Try type aliases
501        if let Some(alias_def) = source_module.type_aliases.get(item_name) {
502            return Ok(Some(self.build_type_alias(source_module_name, alias_def)?));
503        }
504
505        // Try variables
506        if let Some(var_def) = source_module.variables.get(item_name) {
507            return Ok(Some(self.build_variable(source_module_name, var_def)?));
508        }
509
510        Ok(None)
511    }
512
513    /// Correct link targets in a re-exported item to point to the target module
514    fn correct_link_targets(&self, item: &mut DocItem, _target_module: &str) {
515        match item {
516            DocItem::Function(func) => {
517                for sig in &mut func.signatures {
518                    if let Some(ret) = &mut sig.return_type {
519                        self.correct_type_expr(ret);
520                    }
521                    for param in &mut sig.parameters {
522                        self.correct_type_expr(&mut param.type_);
523                    }
524                }
525            }
526            DocItem::Class(cls) => {
527                for base in &mut cls.bases {
528                    self.correct_type_expr(base);
529                }
530                for method in &mut cls.methods {
531                    for sig in &mut method.signatures {
532                        if let Some(ret) = &mut sig.return_type {
533                            self.correct_type_expr(ret);
534                        }
535                        for param in &mut sig.parameters {
536                            self.correct_type_expr(&mut param.type_);
537                        }
538                    }
539                }
540                for attr in &mut cls.attributes {
541                    if let Some(type_) = &mut attr.type_ {
542                        self.correct_type_expr(type_);
543                    }
544                }
545            }
546            DocItem::TypeAlias(alias) => {
547                self.correct_type_expr(&mut alias.definition);
548            }
549            DocItem::Variable(var) => {
550                if let Some(type_) = &mut var.type_ {
551                    self.correct_type_expr(type_);
552                }
553            }
554            DocItem::Module(_) => {}
555        }
556    }
557
558    /// Correct a type expression to use export_map for link targets
559    fn correct_type_expr(&self, type_expr: &mut DocTypeExpr) {
560        if let Some(link_target) = &mut type_expr.link_target {
561            // Try to find the correct export module and FQN
562            // First try the original FQN
563            let (exported_fqn, exported_module) =
564                if let Some(module) = self.export_map.get(&link_target.fqn) {
565                    (link_target.fqn.clone(), module.clone())
566                } else {
567                    // If not found, try to extract the type name and look for it under other modules
568                    // e.g., "pkg._core.A" -> try "pkg.A"
569                    if let Some(type_name) = link_target.fqn.split('.').next_back() {
570                        // Try each module in export_map to find a match
571                        if let Some((fqn, module)) = self
572                            .export_map
573                            .iter()
574                            .find(|(fqn, _)| fqn.ends_with(&format!(".{}", type_name)))
575                        {
576                            (fqn.clone(), module.clone())
577                        } else {
578                            (link_target.fqn.clone(), link_target.doc_module.clone())
579                        }
580                    } else {
581                        (link_target.fqn.clone(), link_target.doc_module.clone())
582                    }
583                };
584
585            link_target.fqn = exported_fqn;
586            link_target.doc_module = exported_module;
587
588            // Update display text to strip internal module prefixes
589            // e.g., "_core.A" -> "A"
590            type_expr.display = self.strip_internal_module_prefix(&type_expr.display);
591        }
592        for child in &mut type_expr.children {
593            self.correct_type_expr(child);
594        }
595    }
596
597    /// Strip internal module prefixes (modules starting with '_') from display text
598    /// e.g., "_core.A" -> "A", "_internal.Foo" -> "Foo"
599    fn strip_internal_module_prefix(&self, display: &str) -> String {
600        prefix_stripper::strip_internal_prefixes(display)
601    }
602}