cargo_docs_md/types.rs
1//! Type rendering utilities for converting rustdoc types to string representations.
2//!
3//! This module provides the [`TypeRenderer`] struct to convert the complex type
4//! structures from rustdoc JSON into human-readable Rust type syntax. These
5//! rendered strings are used in the generated markdown documentation.
6//!
7//! # Overview
8//!
9//! Rustdoc JSON represents types as a tree structure (the `Type` enum). The
10//! [`TypeRenderer`] walks that tree and produces the string representation
11//! you'd write in code.
12//!
13//! # Usage
14//!
15//! ```ignore
16//! let renderer = TypeRenderer::new(&krate);
17//! let type_string = renderer.render_type(&some_type);
18//! let generics = renderer.render_generics(&generic_params);
19//! ```
20//!
21//! # Example Transformations
22//!
23//! | Rustdoc Type | Rendered String |
24//! |--------------|-----------------|
25//! | `Type::Primitive("u32")` | `"u32"` |
26//! | `Type::BorrowedRef { lifetime: Some("'a"), is_mutable: true, type_: ... }` | `"&'a mut T"` |
27//! | `Type::ResolvedPath { path: "Vec", args: ... }` | `"Vec<T>"` |
28//!
29//! # Performance
30//!
31//! Uses `Cow<str>` to avoid allocations for simple types like primitives,
32//! generics, and inferred types. Complex types that require string building
33//! return owned strings.
34
35use std::borrow::Cow;
36
37use rustdoc_types::{
38 AssocItemConstraint, AssocItemConstraintKind, Crate, GenericArg, GenericArgs, GenericBound,
39 GenericParamDef, GenericParamDefKind, Term, TraitBoundModifier, Type,
40};
41
42/// Type renderer for converting rustdoc types to Rust syntax strings.
43///
44/// This struct holds a reference to the crate context and provides methods
45/// to render various type constructs into their string representations.
46///
47/// # Design Note
48///
49/// The `krate` field is currently unused because the `Type` enum is self-contained.
50/// However, it is retained for:
51/// - **Future-proofing**: May need to look up items in `krate.index` for enhanced rendering
52/// - **API consistency**: Signals that the renderer is bound to a specific crate context
53/// - **Type safety**: Prevents accidentally mixing renderers across different crates
54///
55/// # Example
56///
57/// ```ignore
58/// let renderer = TypeRenderer::new(&krate);
59/// let type_str = renderer.render_type(&some_type);
60/// let generics = renderer.render_generics(¶ms);
61/// ```
62#[derive(Debug, Clone, Copy)]
63pub struct TypeRenderer<'a> {
64 /// Reference to the crate for looking up type information.
65 ///
66 /// Reserved for future use (e.g., resolving paths, getting item metadata).
67 #[expect(dead_code, reason = "TODO: Reserved for future use.")]
68 krate: &'a Crate,
69}
70
71impl<'a> TypeRenderer<'a> {
72 /// Create a new type renderer with the given crate context.
73 ///
74 /// # Arguments
75 ///
76 /// * `krate` - The parsed rustdoc crate containing type definitions
77 #[must_use]
78 pub const fn new(krate: &'a Crate) -> Self {
79 Self { krate }
80 }
81
82 /// Render a rustdoc `Type` to its Rust syntax string representation.
83 ///
84 /// This is the main entry point for type rendering. It handles all variants
85 /// of the `Type` enum and recursively renders nested types.
86 ///
87 /// # Arguments
88 ///
89 /// * `ty` - The type to render
90 ///
91 /// # Returns
92 ///
93 /// A `Cow<str>` representing the type in Rust syntax. Simple types like
94 /// primitives and generics return borrowed strings to avoid allocation.
95 ///
96 /// # Supported Type Variants
97 ///
98 /// - Primitives: `u32`, `bool`, `str`, etc.
99 /// - References: `&T`, `&'a mut T`
100 /// - Pointers: `*const T`, `*mut T`
101 /// - Slices and arrays: `[T]`, `[T; N]`
102 /// - Tuples: `(A, B, C)`
103 /// - Paths: `std::vec::Vec<T>`
104 /// - Function pointers: `fn(A, B) -> C`
105 /// - Trait objects: `dyn Trait + Send`
106 /// - Impl trait: `impl Iterator<Item = T>`
107 /// - Qualified paths: `<T as Trait>::Item`
108 #[must_use]
109 pub fn render_type<'t>(&self, ty: &'t Type) -> Cow<'t, str> {
110 match ty {
111 // Named type path like `Vec<T>` or `std::collections::HashMap<K, V>`
112 Type::ResolvedPath(path) => {
113 // If no generic args, borrow the path directly
114 if path.args.is_none() {
115 return Cow::Borrowed(&path.path);
116 }
117 let mut result = path.path.clone();
118 // Append generic arguments if present
119 if let Some(args) = &path.args {
120 result.push_str(&self.render_generic_args(args));
121 }
122 Cow::Owned(result)
123 },
124
125 // Trait object: `dyn Trait + OtherTrait`
126 Type::DynTrait(dyn_trait) => {
127 let traits: Vec<Cow<str>> = dyn_trait
128 .traits
129 .iter()
130 .map(|pt| {
131 if pt.trait_.args.is_none() {
132 Cow::Borrowed(pt.trait_.path.as_str())
133 } else {
134 let mut s = pt.trait_.path.clone();
135 if let Some(args) = &pt.trait_.args {
136 s.push_str(&self.render_generic_args(args));
137 }
138 Cow::Owned(s)
139 }
140 })
141 .collect();
142 Cow::Owned(format!("dyn {}", traits.join(" + ")))
143 },
144
145 // Generic type parameter: `T`, `U`, etc.
146 // Primitive type: `u32`, `bool`, `str`, etc.
147 // Zero allocation - borrow from input
148 Type::Generic(name) | Type::Primitive(name) => Cow::Borrowed(name),
149
150 // Function pointer: `fn(A, B) -> C`
151 Type::FunctionPointer(fp) => {
152 // Render parameter types (we ignore parameter names for brevity)
153 let params: Vec<Cow<str>> = fp
154 .sig
155 .inputs
156 .iter()
157 .map(|(_, t)| self.render_type(t))
158 .collect();
159 // Render return type if present
160 let ret = fp.sig.output.as_ref().map_or_else(String::new, |output| {
161 format!(" -> {}", self.render_type(output))
162 });
163 Cow::Owned(format!("fn({}){}", params.join(", "), ret))
164 },
165
166 // Tuple type: `(A, B, C)` or unit `()`
167 Type::Tuple(types) => {
168 if types.is_empty() {
169 return Cow::Borrowed("()");
170 }
171 let inner: Vec<Cow<str>> = types.iter().map(|t| self.render_type(t)).collect();
172 Cow::Owned(format!("({})", inner.join(", ")))
173 },
174
175 // Slice type: `[T]`
176 Type::Slice(inner) => Cow::Owned(format!("[{}]", self.render_type(inner))),
177
178 // Array type: `[T; N]` where N is a const expression
179 Type::Array { type_, len } => {
180 Cow::Owned(format!("[{}; {len}]", self.render_type(type_)))
181 },
182
183 // Pattern type (rare): just render the underlying type
184 Type::Pat { type_, .. } => self.render_type(type_),
185
186 // Impl trait in return position: `impl Iterator<Item = T>`
187 Type::ImplTrait(bounds) => {
188 let bound_strs: Vec<Cow<str>> = bounds
189 .iter()
190 .map(|b| self.render_generic_bound(b))
191 .collect();
192 Cow::Owned(format!("impl {}", bound_strs.join(" + ")))
193 },
194
195 // Inferred type: `_` (placeholder in turbofish)
196 // Zero allocation - static string
197 Type::Infer => Cow::Borrowed("_"),
198
199 // Raw pointer: `*const T` or `*mut T`
200 Type::RawPointer { is_mutable, type_ } => {
201 let mutability = if *is_mutable { "mut" } else { "const" };
202 Cow::Owned(format!("*{mutability} {}", self.render_type(type_)))
203 },
204
205 // Reference: `&T`, `&mut T`, `&'a T`, `&'a mut T`
206 Type::BorrowedRef {
207 lifetime,
208 is_mutable,
209 type_,
210 } => {
211 // Optional lifetime annotation
212 let lt = lifetime
213 .as_ref()
214 .map(|l| format!("{l} "))
215 .unwrap_or_default();
216 // Optional mut keyword
217 let mutability = if *is_mutable { "mut " } else { "" };
218 Cow::Owned(format!("&{lt}{mutability}{}", self.render_type(type_)))
219 },
220
221 // Qualified path: `<T as Trait>::Item` or `T::Item`
222 Type::QualifiedPath {
223 name,
224 self_type,
225 trait_,
226 ..
227 } => {
228 let self_ty = self.render_type(self_type);
229 Cow::Owned(trait_.as_ref().map_or_else(
230 || format!("{self_ty}::{name}"),
231 |trait_path| format!("<{self_ty} as {}>::{name}", trait_path.path),
232 ))
233 },
234 }
235 }
236
237 /// Render generic arguments in angle bracket or parenthesized form.
238 ///
239 /// Handles three syntaxes:
240 /// - Angle brackets: `<T, U, Item = V>` (most common)
241 /// - Parenthesized: `(A, B) -> C` (for Fn traits)
242 /// - Return type notation: `(..)` (experimental)
243 fn render_generic_args(self, args: &GenericArgs) -> String {
244 match args {
245 // Standard angle bracket syntax: `<T, U, Item = V>`
246 GenericArgs::AngleBracketed { args, constraints } => {
247 // Collect all generic arguments (types, lifetimes, consts)
248 let mut parts: Vec<Cow<str>> =
249 args.iter().map(|a| self.render_generic_arg(a)).collect();
250
251 // Add associated type constraints (e.g., `Item = u32`)
252 parts.extend(
253 constraints
254 .iter()
255 .map(|c| Cow::Owned(self.render_assoc_item_constraint(c))),
256 );
257
258 if parts.is_empty() {
259 String::new()
260 } else {
261 format!("<{}>", parts.join(", "))
262 }
263 },
264
265 // Parenthesized syntax for Fn traits: `Fn(A, B) -> C`
266 GenericArgs::Parenthesized { inputs, output } => {
267 let input_strs: Vec<Cow<str>> =
268 inputs.iter().map(|t| self.render_type(t)).collect();
269 let ret = output
270 .as_ref()
271 .map(|t| format!(" -> {}", self.render_type(t)))
272 .unwrap_or_default();
273
274 format!("({}){}", input_strs.join(", "), ret)
275 },
276
277 // Return type notation (experimental feature)
278 GenericArgs::ReturnTypeNotation => " (..)".to_string(),
279 }
280 }
281
282 /// Render a single generic argument.
283 ///
284 /// Arguments can be:
285 /// - Lifetimes: `'a`, `'static`
286 /// - Types: `T`, `Vec<u32>`
287 /// - Const values: `N`, `{expr}`
288 /// - Inferred: `_`
289 fn render_generic_arg(self, arg: &GenericArg) -> Cow<'_, str> {
290 match arg {
291 // Zero allocation - borrow from input
292 GenericArg::Lifetime(lt) => Cow::Borrowed(lt),
293
294 GenericArg::Type(ty) => self.render_type(ty),
295
296 // For consts, prefer the computed value; fall back to the expression
297 // These are already owned strings in rustdoc_types
298 GenericArg::Const(c) => Cow::Borrowed(c.value.as_deref().unwrap_or(&c.expr)),
299
300 // Zero allocation - static string
301 GenericArg::Infer => Cow::Borrowed("_"),
302 }
303 }
304
305 /// Render an associated type constraint.
306 ///
307 /// These appear in generic bounds:
308 /// - Equality: `Item = u32`
309 /// - Bound: `Item: Display`
310 fn render_assoc_item_constraint(self, constraint: &AssocItemConstraint) -> String {
311 // The constraint may have its own generic args (rare)
312 let args = constraint
313 .args
314 .as_ref()
315 .map(|a| self.render_generic_args(a))
316 .unwrap_or_default();
317
318 match &constraint.binding {
319 // Equality constraint: `Item = SomeType`
320 AssocItemConstraintKind::Equality(term) => {
321 format!("{}{args} = {}", constraint.name, self.render_term(term))
322 },
323
324 // Bound constraint: `Item: SomeTrait + OtherTrait`
325 AssocItemConstraintKind::Constraint(bounds) => {
326 let bound_strs: Vec<Cow<str>> = bounds
327 .iter()
328 .map(|b| self.render_generic_bound(b))
329 .collect();
330
331 format!("{}{args}: {}", constraint.name, bound_strs.join(" + "))
332 },
333 }
334 }
335
336 /// Render a term, which is either a type or a constant.
337 ///
338 /// Used in associated type constraints like `Item = u32`.
339 fn render_term(self, term: &Term) -> Cow<'_, str> {
340 match term {
341 Term::Type(ty) => self.render_type(ty),
342
343 // For consts, prefer the computed value; fall back to the expression
344 Term::Constant(c) => Cow::Borrowed(c.value.as_deref().unwrap_or(&c.expr)),
345 }
346 }
347
348 /// Render a generic bound (trait bound or lifetime bound).
349 ///
350 /// # Examples
351 ///
352 /// - Trait bound: `Clone`, `Iterator<Item = T>`
353 /// - Modified bound: `?Sized`, `~const Drop`
354 /// - Lifetime bound: `'static`, `'a`
355 #[must_use]
356 pub fn render_generic_bound<'t>(&self, bound: &'t GenericBound) -> Cow<'t, str> {
357 match bound {
358 // Trait bound: `Clone`, `?Sized`, `~const Drop`
359 GenericBound::TraitBound {
360 trait_, modifier, ..
361 } => {
362 // Simple case: no modifier and no generic args - borrow directly
363 if matches!(modifier, TraitBoundModifier::None) && trait_.args.is_none() {
364 return Cow::Borrowed(&trait_.path);
365 }
366
367 // Handle bound modifiers
368 let modifier_str = match modifier {
369 TraitBoundModifier::None => "",
370 TraitBoundModifier::Maybe => "?", // ?Sized
371 TraitBoundModifier::MaybeConst => "~const ", // ~const Trait
372 };
373
374 // Build the trait path with any generic args
375 let mut result = format!("{modifier_str}{}", trait_.path);
376
377 if let Some(args) = &trait_.args {
378 result.push_str(&self.render_generic_args(args));
379 }
380
381 Cow::Owned(result)
382 },
383
384 // Lifetime bound: `'static`, `'a`
385 // Zero allocation - borrow from input
386 GenericBound::Outlives(lt) => Cow::Borrowed(lt),
387
388 // Use bound (experimental) - typically empty in output
389 GenericBound::Use(_) => Cow::Borrowed(""),
390 }
391 }
392
393 /// Render a list of generic parameter definitions.
394 ///
395 /// Produces the `<T, U, const N: usize>` portion of a signature.
396 ///
397 /// # Arguments
398 ///
399 /// * `generics` - The list of generic parameters from rustdoc
400 ///
401 /// # Returns
402 ///
403 /// A string like `<T, U>` or empty string if no generics.
404 ///
405 /// # Filtering
406 ///
407 /// Synthetic parameters (generated by the compiler for `impl Trait`)
408 /// are filtered out since they don't appear in the source.
409 #[must_use]
410 pub fn render_generics(&self, generics: &[GenericParamDef]) -> String {
411 if generics.is_empty() {
412 return String::new();
413 }
414
415 // Filter out synthetic params and render each remaining one
416 let params: Vec<String> = generics
417 .iter()
418 .filter_map(|p| self.render_generic_param_def(p))
419 .collect();
420
421 if params.is_empty() {
422 String::new()
423 } else {
424 format!("<{}>", params.join(", "))
425 }
426 }
427
428 /// Render a single generic parameter definition.
429 ///
430 /// Returns `None` for synthetic parameters (compiler-generated).
431 ///
432 /// # Parameter Kinds
433 ///
434 /// - Lifetime: `'a`, `'a: 'b + 'c`
435 /// - Type: `T`, `T: Clone + Send`
436 /// - Const: `const N: usize`
437 fn render_generic_param_def(self, param: &GenericParamDef) -> Option<String> {
438 match ¶m.kind {
439 // Lifetime parameter: `'a` or `'a: 'b + 'c`
440 GenericParamDefKind::Lifetime { outlives } => {
441 let mut result = param.name.clone();
442
443 // Add outlives bounds if present
444 if !outlives.is_empty() {
445 result.push_str(": ");
446 result.push_str(&outlives.join(" + "));
447 }
448
449 Some(result)
450 },
451
452 // Type parameter: `T` or `T: Clone + Send`
453 GenericParamDefKind::Type {
454 bounds,
455 is_synthetic,
456 ..
457 } => {
458 // Skip synthetic parameters (compiler-generated for impl Trait)
459 if *is_synthetic {
460 return None;
461 }
462
463 let mut result = param.name.clone();
464
465 // Add trait bounds if present
466 if !bounds.is_empty() {
467 let bound_strs: Vec<Cow<str>> = bounds
468 .iter()
469 .map(|b| self.render_generic_bound(b))
470 .collect();
471 result.push_str(": ");
472 result.push_str(&bound_strs.join(" + "));
473 }
474
475 Some(result)
476 },
477
478 // Const parameter: `const N: usize`
479 GenericParamDefKind::Const { type_, .. } => {
480 Some(format!("const {}: {}", param.name, self.render_type(type_)))
481 },
482 }
483 }
484
485 /// Render where clause predicates.
486 ///
487 /// Produces the `where T: Clone, U: Send` portion of a signature.
488 ///
489 /// # Arguments
490 ///
491 /// * `where_predicates` - The list of where predicates from rustdoc
492 ///
493 /// # Returns
494 ///
495 /// A formatted where clause string, or empty string if no predicates.
496 ///
497 /// # Format
498 ///
499 /// ```text
500 /// where
501 /// T: Clone,
502 /// U: Send
503 /// ```
504 #[must_use]
505 pub fn render_where_clause(
506 &self,
507 where_predicates: &[rustdoc_types::WherePredicate],
508 ) -> String {
509 if where_predicates.is_empty() {
510 return String::new();
511 }
512
513 let clauses: Vec<String> = where_predicates
514 .iter()
515 .map(|p| self.render_where_predicate(p))
516 .collect();
517
518 // Format with newline and indentation for readability
519 format!("\nwhere\n {}", clauses.join(",\n "))
520 }
521
522 /// Render a single where predicate.
523 ///
524 /// # Predicate Types
525 ///
526 /// - Bound: `T: Clone + Send`
527 /// - Lifetime: `'a: 'b + 'c`
528 /// - Equality: `<T as Iterator>::Item = u32`
529 fn render_where_predicate(self, pred: &rustdoc_types::WherePredicate) -> String {
530 match pred {
531 // Type bound predicate: `T: Clone + Send`
532 rustdoc_types::WherePredicate::BoundPredicate { type_, bounds, .. } => {
533 let bound_strs: Vec<Cow<str>> = bounds
534 .iter()
535 .map(|b| self.render_generic_bound(b))
536 .collect();
537
538 format!("{}: {}", self.render_type(type_), bound_strs.join(" + "))
539 },
540
541 // Lifetime predicate: `'a: 'b + 'c`
542 rustdoc_types::WherePredicate::LifetimePredicate { lifetime, outlives } => {
543 format!("{lifetime}: {}", outlives.join(" + "))
544 },
545
546 // Equality predicate: `<T as Trait>::Item = SomeType`
547 rustdoc_types::WherePredicate::EqPredicate { lhs, rhs } => {
548 format!("{} = {}", self.render_type(lhs), self.render_term(rhs))
549 },
550 }
551 }
552
553 /// Collect all linkable type names from a type.
554 ///
555 /// This extracts type names that could potentially be linked to their definitions.
556 /// Returns a set of (`ype_name`, `type_id`) pairs for `ResolvedPath` types.
557 ///
558 /// # Linkable Types
559 ///
560 /// - `ResolvedPath` types (e.g., `Vec`, `HashMap`, `MyStruct`)
561 /// - Nested types within generics, references, slices, etc.
562 ///
563 /// # Excluded
564 ///
565 /// - Primitives (e.g., `u32`, `bool`)
566 /// - Generic parameters (e.g., `T`, `U`)
567 /// - Inferred types (`_`)
568 #[must_use]
569 pub fn collect_linkable_types(&self, ty: &Type) -> Vec<(String, rustdoc_types::Id)> {
570 let mut result = Vec::new();
571 self.collect_types_recursive(ty, &mut result);
572 result
573 }
574
575 /// Recursively collect linkable types from a type tree.
576 fn collect_types_recursive(self, ty: &Type, result: &mut Vec<(String, rustdoc_types::Id)>) {
577 match ty {
578 Type::ResolvedPath(path) => {
579 // Extract the simple name (last segment of the path)
580 let name = path.path.split("::").last().unwrap_or(&path.path);
581 result.push((name.to_string(), path.id));
582
583 // Also collect from generic arguments
584 if let Some(args) = &path.args {
585 self.collect_from_generic_args(args, result);
586 }
587 },
588
589 Type::DynTrait(dyn_trait) => {
590 for pt in &dyn_trait.traits {
591 let name = pt.trait_.path.split("::").last().unwrap_or(&pt.trait_.path);
592 result.push((name.to_string(), pt.trait_.id));
593
594 if let Some(args) = &pt.trait_.args {
595 self.collect_from_generic_args(args, result);
596 }
597 }
598 },
599
600 Type::BorrowedRef { type_, .. } | Type::RawPointer { type_, .. } => {
601 self.collect_types_recursive(type_, result);
602 },
603
604 Type::Slice(inner)
605 | Type::Array { type_: inner, .. }
606 | Type::Pat { type_: inner, .. } => {
607 self.collect_types_recursive(inner, result);
608 },
609
610 Type::Tuple(types) => {
611 for inner in types {
612 self.collect_types_recursive(inner, result);
613 }
614 },
615
616 Type::FunctionPointer(fp) => {
617 for (_, input_ty) in &fp.sig.inputs {
618 self.collect_types_recursive(input_ty, result);
619 }
620 if let Some(output) = &fp.sig.output {
621 self.collect_types_recursive(output, result);
622 }
623 },
624
625 Type::ImplTrait(bounds) => {
626 for bound in bounds {
627 if let GenericBound::TraitBound { trait_, .. } = bound {
628 let name = trait_.path.split("::").last().unwrap_or(&trait_.path);
629 result.push((name.to_string(), trait_.id));
630
631 if let Some(args) = &trait_.args {
632 self.collect_from_generic_args(args, result);
633 }
634 }
635 }
636 },
637
638 Type::QualifiedPath {
639 self_type, trait_, ..
640 } => {
641 self.collect_types_recursive(self_type, result);
642 if let Some(t) = trait_ {
643 let name = t.path.split("::").last().unwrap_or(&t.path);
644 result.push((name.to_string(), t.id));
645 }
646 },
647
648 // Primitives, generics, and inferred types are not linkable
649 Type::Primitive(_) | Type::Generic(_) | Type::Infer => {},
650 }
651 }
652
653 /// Collect types from generic arguments.
654 fn collect_from_generic_args(
655 self,
656 args: &GenericArgs,
657 result: &mut Vec<(String, rustdoc_types::Id)>,
658 ) {
659 match args {
660 GenericArgs::AngleBracketed { args, constraints } => {
661 for arg in args {
662 if let GenericArg::Type(ty) = arg {
663 self.collect_types_recursive(ty, result);
664 }
665 }
666 for constraint in constraints {
667 if let AssocItemConstraintKind::Equality(Term::Type(ty)) = &constraint.binding {
668 self.collect_types_recursive(ty, result);
669 }
670 }
671 },
672 GenericArgs::Parenthesized { inputs, output } => {
673 for input in inputs {
674 self.collect_types_recursive(input, result);
675 }
676 if let Some(output) = output {
677 self.collect_types_recursive(output, result);
678 }
679 },
680 GenericArgs::ReturnTypeNotation => {},
681 }
682 }
683}