Skip to main content

sourcey_rustdoc/
extract.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use rustdoc_types::{Crate, Id, Item as RdItem, ItemEnum, StructKind, VariantKind, Visibility as RdVisibility};
4
5use crate::diagnostics::{self, codes};
6use crate::doctest::extract_doctests;
7use crate::links::{format_id, LinkContext};
8use crate::signature::{lower_generics, lower_signature, render_type};
9use crate::spec::{
10    AssocConstItem, AssocTypeItem, ConstantItem, CrateSpec, Deprecation, Diagnostic, EnumItem,
11    FunctionItem, ImplItem, Item, ItemId, ItemInner, LinkTarget, MacroItem, ModuleSpec,
12    ProcMacroItem, ProcMacroKindKind, SourceLocation, Stability, StabilityLevel, StaticItem,
13    StructFieldItem, StructItem, StructKindKind, TraitAliasItem, TraitItem, TypeAliasItem,
14    TypePath, UnionItem, UseItem, VariantItem, VariantKindKind, Visibility,
15};
16
17pub struct ExtractOptions {
18    pub include_private: bool,
19    pub include_hidden: bool,
20    pub crate_name_hint: String,
21}
22
23pub struct ExtractResult {
24    pub crate_spec: CrateSpec,
25    pub diagnostics: Vec<Diagnostic>,
26}
27
28pub fn extract_crate(krate: &Crate, opts: &ExtractOptions) -> ExtractResult {
29    let links = LinkContext::new(krate);
30    let mut diagnostics: Vec<Diagnostic> = Vec::new();
31    let mut items: BTreeMap<String, Item> = BTreeMap::new();
32    let mut modules: Vec<ModuleSpec> = Vec::new();
33    let mut emitted: HashSet<ItemId> = HashSet::new();
34
35    for (id, rd_item) in &krate.index {
36        if !should_emit(rd_item, opts) {
37            continue;
38        }
39        let item_id = ItemId(format_id(id));
40        if !emitted.insert(item_id.clone()) {
41            continue;
42        }
43        match &rd_item.inner {
44            ItemEnum::Module(m) => {
45                modules.push(lower_module(id, rd_item, m, krate));
46                // Also surface modules as items so per-module sidebar entries,
47                // search categories, and parent-module pages can link to
48                // their children.
49                if let Some(item) = lower_item(id, rd_item, krate, &links, &mut diagnostics) {
50                    items.insert(item.id.0.clone(), item);
51                }
52            }
53            _ => {
54                if let Some(item) = lower_item(id, rd_item, krate, &links, &mut diagnostics) {
55                    items.insert(item.id.0.clone(), item);
56                }
57            }
58        }
59    }
60
61    let root_module_id = ItemId(format_id(&krate.root));
62    // Prefer the user-supplied crate name (matches Cargo.toml `[package].name`,
63    // hyphens preserved). Fall back to rustdoc's lib target name (underscored)
64    // when no hint is set.
65    let crate_name = if opts.crate_name_hint.is_empty() {
66        krate
67            .index
68            .get(&krate.root)
69            .and_then(|i| i.name.clone())
70            .unwrap_or_default()
71    } else {
72        opts.crate_name_hint.clone()
73    };
74    let crate_version = krate.crate_version.clone();
75
76    let crate_spec = CrateSpec {
77        name: crate_name,
78        version: crate_version,
79        root_module_id,
80        modules,
81        items,
82        external_crates: links.external_crates(),
83        diagnostics: diagnostics.clone(),
84    };
85    if diagnostics.iter().any(|d| d.code == codes::INTRA_DOC_LINK_UNRESOLVED) {
86        // Carry intra-doc-link diagnostics into the crate-level diagnostics
87        // (already in `crate_spec.diagnostics`); we return the same set in the
88        // top-level summary for caller-level visibility.
89    }
90    ExtractResult {
91        crate_spec,
92        diagnostics,
93    }
94}
95
96fn should_emit(item: &RdItem, opts: &ExtractOptions) -> bool {
97    if !opts.include_hidden {
98        for attr in &item.attrs {
99            if let rustdoc_types::Attribute::Other(s) = attr {
100                if s.contains("doc(hidden)") {
101                    return false;
102                }
103            }
104        }
105    }
106    if opts.include_private {
107        return true;
108    }
109    // Without --document-private-items, rustdoc's `index` only contains
110    // items it considers documented (public, or implicitly public via a
111    // public parent). Most items inside impl blocks, trait assoc items,
112    // enum variants, and struct fields carry `Default` visibility but are
113    // still public because their parent is. Trust rustdoc's filtering and
114    // only drop explicit `Crate`/`Restricted` visibility.
115    match &item.visibility {
116        RdVisibility::Public | RdVisibility::Default => true,
117        RdVisibility::Crate | RdVisibility::Restricted { .. } => false,
118    }
119}
120
121fn lower_module(id: &Id, item: &RdItem, m: &rustdoc_types::Module, krate: &Crate) -> ModuleSpec {
122    let path = path_for_id(id, krate);
123    let item_ids: Vec<ItemId> = m.items.iter().map(|i| ItemId(format_id(i))).collect();
124    let sub_module_paths: Vec<Vec<String>> = m
125        .items
126        .iter()
127        .filter_map(|i| krate.index.get(i))
128        .filter(|child| matches!(child.inner, ItemEnum::Module(_)))
129        .map(|child| {
130            let mut p = path.clone();
131            if let Some(name) = &child.name {
132                p.push(name.clone());
133            }
134            p
135        })
136        .collect();
137    ModuleSpec {
138        id: ItemId(format_id(id)),
139        path,
140        docs_markdown: item.docs.clone(),
141        doc_aliases: extract_doc_aliases(item),
142        item_ids,
143        sub_module_paths,
144        source: lower_source(item),
145    }
146}
147
148fn lower_item(
149    id: &Id,
150    rd_item: &RdItem,
151    krate: &Crate,
152    links: &LinkContext<'_>,
153    diagnostics: &mut Vec<Diagnostic>,
154) -> Option<Item> {
155    let item_id = ItemId(format_id(id));
156    let inner = lower_inner(rd_item, krate, links, diagnostics)?;
157    let docs_markdown = rd_item.docs.clone();
158    let doctests = docs_markdown
159        .as_deref()
160        .map(extract_doctests)
161        .unwrap_or_default();
162    let item = Item {
163        id: item_id,
164        name: rd_item.name.clone(),
165        path: path_for_id(id, krate),
166        visibility: lower_visibility(&rd_item.visibility),
167        source: lower_source(rd_item),
168        docs_markdown,
169        doc_aliases: extract_doc_aliases(rd_item),
170        deprecation: rd_item.deprecation.as_ref().map(|d| Deprecation {
171            since: d.since.clone(),
172            note: d.note.clone(),
173        }),
174        stability: extract_stability(rd_item),
175        feature_gates: extract_feature_gates(rd_item),
176        attrs_structured: extract_structured_attrs(rd_item),
177        links: rd_item
178            .links
179            .iter()
180            .map(|(label, id)| {
181                let target = links.resolve_id(id).map(|resolved| {
182                    if resolved.external {
183                        LinkTarget::External {
184                            crate_name: links
185                                .external_crate_index
186                                .get(&resolved.crate_id)
187                                .map(|ec| ec.name.clone())
188                                .unwrap_or_default(),
189                            path: resolved.path,
190                            html_root_url: resolved.html_root_url,
191                            item_kind: Some(resolved.kind),
192                        }
193                    } else {
194                        LinkTarget::Internal { id: resolved.id }
195                    }
196                }).unwrap_or(LinkTarget::Internal { id: ItemId(format_id(id)) });
197                (label.clone(), target)
198            })
199            .collect(),
200        inner,
201        doctests,
202    };
203    Some(item)
204}
205
206fn lower_inner(
207    rd_item: &RdItem,
208    krate: &Crate,
209    links: &LinkContext<'_>,
210    diagnostics: &mut Vec<Diagnostic>,
211) -> Option<ItemInner> {
212    let inner = match &rd_item.inner {
213        ItemEnum::Function(f) => ItemInner::Function(FunctionItem {
214            signature: lower_signature(
215                &f.sig,
216                rd_item.name.as_deref().unwrap_or(""),
217                f.header.is_const,
218                f.header.is_async,
219                f.header.is_unsafe,
220                links,
221            ),
222            generics: lower_generics(&f.generics),
223            is_const: f.header.is_const,
224            is_async: f.header.is_async,
225            is_unsafe: f.header.is_unsafe,
226            has_body: f.has_body,
227        }),
228        ItemEnum::Struct(s) => ItemInner::Struct(StructItem {
229            struct_kind: match &s.kind {
230                StructKind::Plain { .. } => StructKindKind::Plain,
231                StructKind::Tuple(_) => StructKindKind::Tuple,
232                StructKind::Unit => StructKindKind::Unit,
233            },
234            generics: lower_generics(&s.generics),
235            fields: extract_struct_fields(&s.kind),
236            has_stripped_fields: matches!(
237                &s.kind,
238                StructKind::Plain {
239                    has_stripped_fields: true,
240                    ..
241                }
242            ),
243            impls: s.impls.iter().map(|i| ItemId(format_id(i))).collect(),
244        }),
245        ItemEnum::Enum(e) => ItemInner::Enum(EnumItem {
246            generics: lower_generics(&e.generics),
247            variants: e.variants.iter().map(|i| ItemId(format_id(i))).collect(),
248            has_stripped_variants: e.has_stripped_variants,
249            impls: e.impls.iter().map(|i| ItemId(format_id(i))).collect(),
250        }),
251        ItemEnum::Variant(v) => ItemInner::Variant(VariantItem {
252            variant_kind: match &v.kind {
253                VariantKind::Plain => VariantKindKind::Plain,
254                VariantKind::Tuple(_) => VariantKindKind::Tuple,
255                VariantKind::Struct { .. } => VariantKindKind::Struct,
256            },
257            discriminant: v.discriminant.as_ref().map(|d| d.expr.clone()),
258        }),
259        ItemEnum::Union(u) => ItemInner::Union(UnionItem {
260            generics: lower_generics(&u.generics),
261            fields: u.fields.iter().map(|i| ItemId(format_id(i))).collect(),
262            has_stripped_fields: u.has_stripped_fields,
263            impls: u.impls.iter().map(|i| ItemId(format_id(i))).collect(),
264        }),
265        ItemEnum::Trait(t) => ItemInner::Trait(TraitItem {
266            is_auto: t.is_auto,
267            is_unsafe: t.is_unsafe,
268            is_dyn_compatible: t.is_dyn_compatible,
269            generics: lower_generics(&t.generics),
270            bounds: t.bounds.iter().map(crate::signature::render_bound).collect(),
271            items: t.items.iter().map(|i| ItemId(format_id(i))).collect(),
272            implementations: t
273                .implementations
274                .iter()
275                .map(|i| ItemId(format_id(i)))
276                .collect(),
277        }),
278        ItemEnum::TraitAlias(a) => ItemInner::TraitAlias(TraitAliasItem {
279            generics: lower_generics(&a.generics),
280            bounds: a.params.iter().map(crate::signature::render_bound).collect(),
281        }),
282        ItemEnum::Impl(i) => ItemInner::Impl(ImplItem {
283            generics: lower_generics(&i.generics),
284            trait_path: i.trait_.as_ref().map(|p| type_path_from_resolved(p, links)),
285            for_type: type_path_for_type(&i.for_, links),
286            items: i.items.iter().map(|x| ItemId(format_id(x))).collect(),
287            is_negative: i.is_negative,
288            is_synthetic: i.is_synthetic,
289            is_blanket: i.blanket_impl.is_some(),
290            provided_trait_methods: i.provided_trait_methods.clone(),
291        }),
292        ItemEnum::TypeAlias(a) => ItemInner::TypeAlias(TypeAliasItem {
293            aliased_type: type_path_for_type(&a.type_, links),
294            generics: lower_generics(&a.generics),
295        }),
296        ItemEnum::Constant { type_, const_ } => ItemInner::Constant(ConstantItem {
297            type_display: render_type(type_),
298            expr: const_.expr.clone(),
299            value: const_.value.clone(),
300            is_literal: const_.is_literal,
301        }),
302        ItemEnum::Static(s) => ItemInner::Static(StaticItem {
303            type_display: render_type(&s.type_),
304            expr: s.expr.clone(),
305            is_mutable: s.is_mutable,
306            is_unsafe: s.is_unsafe,
307        }),
308        ItemEnum::Macro(source) => ItemInner::Macro(MacroItem {
309            source: source.clone(),
310        }),
311        ItemEnum::ProcMacro(pm) => ItemInner::ProcMacro(ProcMacroItem {
312            macro_kind: match pm.kind {
313                rustdoc_types::MacroKind::Bang => ProcMacroKindKind::Bang,
314                rustdoc_types::MacroKind::Attr => ProcMacroKindKind::Attr,
315                rustdoc_types::MacroKind::Derive => ProcMacroKindKind::Derive,
316            },
317            helpers: pm.helpers.clone(),
318        }),
319        ItemEnum::AssocType {
320            generics,
321            bounds,
322            type_,
323        } => ItemInner::AssocType(AssocTypeItem {
324            generics: lower_generics(generics),
325            bounds: bounds.iter().map(crate::signature::render_bound).collect(),
326            default_display: type_.as_ref().map(render_type),
327        }),
328        ItemEnum::AssocConst { type_, value } => ItemInner::AssocConst(AssocConstItem {
329            type_display: render_type(type_),
330            default_display: value.clone(),
331        }),
332        ItemEnum::Use(u) => ItemInner::Use(UseItem {
333            source: u.source.clone(),
334            name: u.name.clone(),
335            target_id: u.id.as_ref().map(|i| ItemId(format_id(i))),
336            is_glob: u.is_glob,
337        }),
338        ItemEnum::StructField(t) => ItemInner::StructField(StructFieldItem {
339            type_display: render_type(t),
340        }),
341        ItemEnum::Module(_) => ItemInner::Module,
342        ItemEnum::Primitive(_) => ItemInner::Primitive,
343        ItemEnum::ExternType => ItemInner::ExternType,
344        ItemEnum::ExternCrate { .. } => return None,
345    };
346    let _ = (krate, links, diagnostics);
347    Some(inner)
348}
349
350fn lower_visibility(v: &RdVisibility) -> Visibility {
351    match v {
352        RdVisibility::Public => Visibility::Public,
353        RdVisibility::Crate => Visibility::Crate,
354        RdVisibility::Restricted { path, .. } => Visibility::Restricted { path: path.clone() },
355        RdVisibility::Default => Visibility::Default,
356    }
357}
358
359fn lower_source(item: &RdItem) -> Option<SourceLocation> {
360    item.span.as_ref().map(|s| SourceLocation {
361        file: s.filename.to_string_lossy().into_owned(),
362        line_start: s.begin.0 as u32,
363        line_end: s.end.0 as u32,
364    })
365}
366
367fn extract_doc_aliases(item: &RdItem) -> Vec<String> {
368    let mut out: Vec<String> = Vec::new();
369    for attr in &item.attrs {
370        if let rustdoc_types::Attribute::Other(s) = attr {
371            if let Some(rest) = s.strip_prefix("#[doc(alias = \"") {
372                if let Some(end) = rest.find("\")]") {
373                    out.push(rest[..end].to_string());
374                }
375            } else if let Some(rest) = s.strip_prefix("#[doc(alias(") {
376                if let Some(end) = rest.find("))]") {
377                    for raw in rest[..end].split(',') {
378                        let trimmed = raw.trim().trim_matches('"');
379                        if !trimmed.is_empty() {
380                            out.push(trimmed.to_string());
381                        }
382                    }
383                }
384            }
385        }
386    }
387    out
388}
389
390fn extract_stability(item: &RdItem) -> Option<Stability> {
391    for attr in &item.attrs {
392        if let rustdoc_types::Attribute::Other(s) = attr {
393            if s.starts_with("#[stable(") {
394                return Some(Stability {
395                    level: StabilityLevel::Stable,
396                    since: extract_attr_string(s, "since"),
397                    feature: extract_attr_string(s, "feature"),
398                    issue: None,
399                });
400            }
401            if s.starts_with("#[unstable(") {
402                return Some(Stability {
403                    level: StabilityLevel::Unstable,
404                    since: None,
405                    feature: extract_attr_string(s, "feature"),
406                    issue: extract_attr_string(s, "issue")
407                        .and_then(|i| i.trim_matches(|c: char| !c.is_ascii_digit()).parse().ok()),
408                });
409            }
410        }
411    }
412    None
413}
414
415fn extract_attr_string(s: &str, key: &str) -> Option<String> {
416    let needle = format!("{} = \"", key);
417    let start = s.find(&needle)? + needle.len();
418    let rest = &s[start..];
419    let end = rest.find('"')?;
420    Some(rest[..end].to_string())
421}
422
423fn extract_feature_gates(_item: &RdItem) -> Vec<String> {
424    // Phase 1: feature gates are not directly carried on items in rustdoc JSON
425    // until the renderer reconstructs them from cfg attrs. Placeholder so the
426    // field exists in the schema and Phase 3 rendering can fill it in.
427    Vec::new()
428}
429
430fn extract_structured_attrs(item: &RdItem) -> Vec<String> {
431    item.attrs
432        .iter()
433        .map(|attr| match attr {
434            rustdoc_types::Attribute::Other(s) => s.clone(),
435            other => format!("{:?}", other),
436        })
437        .collect()
438}
439
440fn extract_struct_fields(kind: &StructKind) -> Vec<ItemId> {
441    match kind {
442        StructKind::Plain { fields, .. } => fields.iter().map(|i| ItemId(format_id(i))).collect(),
443        StructKind::Tuple(ids) => ids
444            .iter()
445            .filter_map(|maybe| maybe.as_ref().map(|i| ItemId(format_id(i))))
446            .collect(),
447        StructKind::Unit => Vec::new(),
448    }
449}
450
451fn type_path_from_resolved(p: &rustdoc_types::Path, links: &LinkContext<'_>) -> TypePath {
452    let resolved = links.resolve_id(&p.id);
453    TypePath {
454        crate_id: resolved.as_ref().map(|r| r.crate_id).unwrap_or(0),
455        item_kind: resolved.as_ref().map(|r| r.kind.clone()),
456        path: resolved.map(|r| r.path).unwrap_or_else(|| vec![p.path.clone()]),
457        display: p.path.clone(),
458        external: false,
459        html_root_url: None,
460    }
461}
462
463fn type_path_for_type(ty: &rustdoc_types::Type, links: &LinkContext<'_>) -> TypePath {
464    if let rustdoc_types::Type::ResolvedPath(p) = ty {
465        return type_path_from_resolved(p, links);
466    }
467    TypePath {
468        crate_id: 0,
469        path: vec![render_type(ty)],
470        display: render_type(ty),
471        external: false,
472        html_root_url: None,
473        item_kind: None,
474    }
475}
476
477fn path_for_id(id: &Id, krate: &Crate) -> Vec<String> {
478    if let Some(summary) = krate.paths.get(id) {
479        return summary.path.clone();
480    }
481    if let Some(item) = krate.index.get(id) {
482        if let Some(name) = &item.name {
483            return vec![name.clone()];
484        }
485    }
486    Vec::new()
487}
488
489#[allow(dead_code)]
490fn unused_diagnostics_marker() -> Diagnostic {
491    diagnostics::warning(codes::INTRA_DOC_LINK_UNRESOLVED, "marker")
492}
493
494// Keep imports referenced even though the immediate use is in helper modules.
495const _: fn(&HashMap<u32, ()>) = |_| {};