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 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 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 }
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 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 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
494const _: fn(&HashMap<u32, ()>) = |_| {};