1use std::collections::{HashMap, HashSet};
4use std::{error::Error, fmt, path::Path};
5
6use docx_store::models::{
7 DocBlock,
8 DocExample,
9 DocParam,
10 DocSection,
11 DocTypeParam,
12 Param,
13 SeeAlso,
14 SourceId,
15 Symbol,
16 TypeParam,
17 TypeRef,
18};
19use docx_store::schema::{SOURCE_KIND_RUSTDOC_JSON, make_symbol_key};
20use serde::Deserialize;
21use serde_json::Value;
22
23#[derive(Debug, Clone)]
25pub struct RustdocParseOptions {
26 pub project_id: String,
27 pub ingest_id: Option<String>,
28 pub language: String,
29 pub source_kind: String,
30}
31
32impl RustdocParseOptions {
33 pub fn new(project_id: impl Into<String>) -> Self {
34 Self {
35 project_id: project_id.into(),
36 ingest_id: None,
37 language: "rust".to_string(),
38 source_kind: SOURCE_KIND_RUSTDOC_JSON.to_string(),
39 }
40 }
41
42 #[must_use]
43 pub fn with_ingest_id(mut self, ingest_id: impl Into<String>) -> Self {
44 self.ingest_id = Some(ingest_id.into());
45 self
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct RustdocParseOutput {
52 pub crate_name: Option<String>,
53 pub symbols: Vec<Symbol>,
54 pub doc_blocks: Vec<DocBlock>,
55}
56
57#[derive(Debug)]
59pub struct RustdocParseError {
60 message: String,
61}
62
63impl RustdocParseError {
64 fn new(message: impl Into<String>) -> Self {
65 Self {
66 message: message.into(),
67 }
68 }
69}
70
71impl fmt::Display for RustdocParseError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "rustdoc JSON parse error: {}", self.message)
74 }
75}
76
77impl Error for RustdocParseError {}
78
79impl From<serde_json::Error> for RustdocParseError {
80 fn from(err: serde_json::Error) -> Self {
81 Self::new(err.to_string())
82 }
83}
84
85impl From<std::io::Error> for RustdocParseError {
86 fn from(err: std::io::Error) -> Self {
87 Self::new(err.to_string())
88 }
89}
90
91impl From<tokio::task::JoinError> for RustdocParseError {
92 fn from(err: tokio::task::JoinError) -> Self {
93 Self::new(err.to_string())
94 }
95}
96
97pub struct RustdocJsonParser;
99
100impl RustdocJsonParser {
101 #[allow(clippy::too_many_lines)]
106 pub fn parse(
107 json: &str,
108 options: &RustdocParseOptions,
109 ) -> Result<RustdocParseOutput, RustdocParseError> {
110 let crate_doc: RustdocCrate = serde_json::from_str(json)?;
111 let root_id = crate_doc.root;
112 let root_item = crate_doc
113 .index
114 .get(&root_id.to_string())
115 .ok_or_else(|| RustdocParseError::new("missing root item"))?;
116
117 let crate_name = root_item.name.clone();
118 let root_crate_id = root_item.crate_id;
119 let mut id_to_path = build_id_path_map(&crate_doc, root_crate_id);
120
121 let mut state = ParserState {
122 crate_doc: &crate_doc,
123 options,
124 root_crate_id,
125 id_to_path: &mut id_to_path,
126 symbols: Vec::new(),
127 doc_blocks: Vec::new(),
128 seen: HashSet::new(),
129 };
130
131 let mut module_path = Vec::new();
132 if let Some(name) = crate_name.clone() {
133 module_path.push(name);
134 }
135 state.visit_module(root_id, &module_path);
136
137 Ok(RustdocParseOutput {
138 crate_name,
139 symbols: state.symbols,
140 doc_blocks: state.doc_blocks,
141 })
142 }
143 pub async fn parse_async(
148 json: String,
149 options: RustdocParseOptions,
150 ) -> Result<RustdocParseOutput, RustdocParseError> {
151 tokio::task::spawn_blocking(move || Self::parse(&json, &options)).await?
152 }
153
154 pub async fn parse_file(
159 path: impl AsRef<Path>,
160 options: RustdocParseOptions,
161 ) -> Result<RustdocParseOutput, RustdocParseError> {
162 let path = path.as_ref().to_path_buf();
163 let json = tokio::task::spawn_blocking(move || std::fs::read_to_string(path)).await??;
164 Self::parse_async(json, options).await
165 }
166}
167
168#[derive(Debug, Deserialize)]
169struct RustdocCrate {
170 root: u64,
171 index: HashMap<String, RustdocItem>,
172 #[serde(default)]
173 paths: HashMap<String, RustdocPath>,
174}
175
176#[derive(Debug, Deserialize, Clone)]
177struct RustdocItem {
178 id: u64,
179 crate_id: u64,
180 name: Option<String>,
181 span: Option<RustdocSpan>,
182 visibility: Option<String>,
183 docs: Option<String>,
184 deprecation: Option<RustdocDeprecation>,
185 inner: HashMap<String, Value>,
186}
187
188#[derive(Debug, Deserialize, Clone)]
189struct RustdocSpan {
190 filename: String,
191 begin: [u32; 2],
192}
193
194#[derive(Debug, Deserialize, Clone)]
195struct RustdocPath {
196 crate_id: u64,
197 path: Vec<String>,
198}
199
200#[derive(Debug, Deserialize, Clone)]
201struct RustdocDeprecation {
202 since: Option<String>,
203}
204
205struct ParserState<'a> {
206 crate_doc: &'a RustdocCrate,
207 options: &'a RustdocParseOptions,
208 root_crate_id: u64,
209 id_to_path: &'a mut HashMap<u64, String>,
210 symbols: Vec<Symbol>,
211 doc_blocks: Vec<DocBlock>,
212 seen: HashSet<u64>,
213}
214impl ParserState<'_> {
215 fn visit_module(&mut self, module_id: u64, module_path: &[String]) {
216 if self.seen.contains(&module_id) {
217 return;
218 }
219 let Some(item) = self.get_item(module_id) else {
220 return;
221 };
222 if item.crate_id != self.root_crate_id {
223 return;
224 }
225 self.seen.insert(module_id);
226
227 self.add_symbol(&item, module_path, None, Some("module"));
228 let items = module_items(&item);
229 for child_id in items {
230 if let Some(child) = self.get_item(child_id) {
231 if child.crate_id != self.root_crate_id {
232 continue;
233 }
234 if is_inner_kind(&child, "module") {
235 let mut child_path = module_path.to_vec();
236 if let Some(name) = child.name.as_ref() && !name.is_empty() {
237 child_path.push(name.clone());
238 }
239 self.visit_module(child_id, &child_path);
240 } else {
241 self.visit_item(child_id, module_path);
242 }
243 }
244 }
245 }
246
247 fn visit_item(&mut self, item_id: u64, module_path: &[String]) {
248 if self.seen.contains(&item_id) {
249 return;
250 }
251 let Some(item) = self.get_item(item_id) else {
252 return;
253 };
254 if item.crate_id != self.root_crate_id {
255 return;
256 }
257 self.seen.insert(item_id);
258
259 let inner_kind = inner_kind(&item);
260 match inner_kind {
261 Some("struct") => {
262 let qualified = self.add_symbol(&item, module_path, None, Some("struct"));
263 self.visit_struct_fields(&item, &qualified);
264 self.visit_impls(&item, &qualified);
265 }
266 Some("enum") => {
267 let qualified = self.add_symbol(&item, module_path, None, Some("enum"));
268 self.visit_enum_variants(&item, &qualified);
269 self.visit_impls(&item, &qualified);
270 }
271 Some("trait") => {
272 let qualified = self.add_symbol(&item, module_path, None, Some("trait"));
273 self.visit_trait_items(&item, &qualified);
274 self.visit_impls(&item, &qualified);
275 }
276 Some("function") => {
277 self.add_symbol(&item, module_path, None, Some("function"));
278 }
279 Some("type_alias") => {
280 self.add_symbol(&item, module_path, None, Some("type_alias"));
281 }
282 Some("constant") => {
283 self.add_symbol(&item, module_path, None, Some("const"));
284 }
285 Some("static") => {
286 self.add_symbol(&item, module_path, None, Some("static"));
287 }
288 Some("union") => {
289 self.add_symbol(&item, module_path, None, Some("union"));
290 }
291 Some("macro") => {
292 self.add_symbol(&item, module_path, None, Some("macro"));
293 }
294 Some("module") => {
295 let mut child_path = module_path.to_vec();
296 if let Some(name) = item.name.as_ref() && !name.is_empty() {
297 child_path.push(name.clone());
298 }
299 self.visit_module(item_id, &child_path);
300 }
301 _ => {}
302 }
303 }
304
305 fn visit_struct_fields(&mut self, item: &RustdocItem, owner_name: &str) {
306 let Some(inner) = item.inner.get("struct") else {
307 return;
308 };
309 let Some(kind) = inner.get("kind") else {
310 return;
311 };
312 let field_ids = struct_kind_fields(kind);
313 for field_id in field_ids {
314 if let Some(field_item) = self.get_item(field_id) {
315 if field_item.crate_id != self.root_crate_id {
316 continue;
317 }
318 self.add_symbol(&field_item, &[], Some(owner_name), Some("field"));
319 }
320 }
321 }
322
323 fn visit_enum_variants(&mut self, item: &RustdocItem, owner_name: &str) {
324 let Some(inner) = item.inner.get("enum") else {
325 return;
326 };
327 let Some(variants) = inner.get("variants").and_then(Value::as_array) else {
328 return;
329 };
330 for variant_id in variants.iter().filter_map(Value::as_u64) {
331 if let Some(variant_item) = self.get_item(variant_id) {
332 if variant_item.crate_id != self.root_crate_id {
333 continue;
334 }
335 self.add_symbol(&variant_item, &[], Some(owner_name), Some("variant"));
336 }
337 }
338 }
339
340 fn visit_trait_items(&mut self, item: &RustdocItem, owner_name: &str) {
341 let Some(inner) = item.inner.get("trait") else {
342 return;
343 };
344 let Some(items) = inner.get("items").and_then(Value::as_array) else {
345 return;
346 };
347 for assoc_id in items.iter().filter_map(Value::as_u64) {
348 if let Some(assoc_item) = self.get_item(assoc_id) {
349 if assoc_item.crate_id != self.root_crate_id {
350 continue;
351 }
352 self.add_symbol(&assoc_item, &[], Some(owner_name), Some("trait_item"));
353 }
354 }
355 }
356
357 fn visit_impls(&mut self, item: &RustdocItem, owner_name: &str) {
358 let impl_ids = match inner_kind(item) {
359 Some("struct") => item
360 .inner
361 .get("struct")
362 .and_then(|value| value.get("impls"))
363 .and_then(Value::as_array)
364 .map(|items| extract_ids(items)),
365 Some("enum") => item
366 .inner
367 .get("enum")
368 .and_then(|value| value.get("impls"))
369 .and_then(Value::as_array)
370 .map(|items| extract_ids(items)),
371 Some("trait") => item
372 .inner
373 .get("trait")
374 .and_then(|value| value.get("impls"))
375 .and_then(Value::as_array)
376 .map(|items| extract_ids(items)),
377 _ => None,
378 };
379
380 let Some(impl_ids) = impl_ids else {
381 return;
382 };
383
384 for impl_id in impl_ids {
385 let Some(impl_item) = self.get_item(impl_id) else {
386 continue;
387 };
388 if impl_item.crate_id != self.root_crate_id {
389 continue;
390 }
391 let Some(impl_inner) = impl_item.inner.get("impl") else {
392 continue;
393 };
394 let Some(items) = impl_inner.get("items").and_then(Value::as_array) else {
395 continue;
396 };
397 for assoc_id in items.iter().filter_map(Value::as_u64) {
398 if let Some(assoc_item) = self.get_item(assoc_id) {
399 if assoc_item.crate_id != self.root_crate_id {
400 continue;
401 }
402 self.add_symbol(&assoc_item, &[], Some(owner_name), Some("method"));
403 }
404 }
405 }
406 }
407
408 fn add_symbol(
409 &mut self,
410 item: &RustdocItem,
411 module_path: &[String],
412 owner_name: Option<&str>,
413 kind_override: Option<&str>,
414 ) -> String {
415 let name = item.name.clone().unwrap_or_default();
416 let qualified_name = qualified_name_for_item(&name, module_path, owner_name);
417
418 let symbol_key = make_symbol_key("rust", &self.options.project_id, &qualified_name);
419 let doc_symbol_key = symbol_key.clone();
420 self.id_to_path.insert(item.id, qualified_name.clone());
421
422 let docs = item.docs.as_deref().unwrap_or("").trim();
423 let parsed_docs = (!docs.is_empty()).then(|| parse_markdown_docs(docs));
424
425 let (params, return_type, signature) = parse_signature(item, self, &name);
426 let type_params = parse_type_params(item);
427 let (source_path, line, col) = span_location(item);
428
429 let parts = SymbolParts {
430 name,
431 qualified_name: qualified_name.clone(),
432 symbol_key,
433 signature,
434 params,
435 return_type,
436 type_params,
437 source_path,
438 line,
439 col,
440 };
441
442 let symbol = build_symbol(item, self.options, parts, kind_override, parsed_docs.as_ref());
443 self.symbols.push(symbol);
444
445 if let Some(parsed_docs) = parsed_docs {
446 let doc_block = build_doc_block(self.options, doc_symbol_key, parsed_docs, docs);
447 self.doc_blocks.push(doc_block);
448 }
449
450 qualified_name
451 }
452
453 fn get_item(&self, item_id: u64) -> Option<RustdocItem> {
454 self.crate_doc
455 .index
456 .get(&item_id.to_string())
457 .cloned()
458 }
459}
460
461fn qualified_name_for_item(
462 name: &str,
463 module_path: &[String],
464 owner_name: Option<&str>,
465) -> String {
466 owner_name.map_or_else(
467 || {
468 if module_path.is_empty() {
469 name.to_string()
470 } else if name.is_empty() {
471 module_path.join("::")
472 } else {
473 format!("{}::{name}", module_path.join("::"))
474 }
475 },
476 |owner| {
477 if name.is_empty() {
478 owner.to_string()
479 } else {
480 format!("{owner}::{name}")
481 }
482 },
483 )
484}
485
486fn span_location(item: &RustdocItem) -> (Option<String>, Option<u32>, Option<u32>) {
487 item.span.as_ref().map_or((None, None, None), |span| {
488 (
489 Some(span.filename.clone()),
490 Some(span.begin[0]),
491 Some(span.begin[1]),
492 )
493 })
494}
495
496struct SymbolParts {
497 name: String,
498 qualified_name: String,
499 symbol_key: String,
500 signature: Option<String>,
501 params: Vec<Param>,
502 return_type: Option<TypeRef>,
503 type_params: Vec<TypeParam>,
504 source_path: Option<String>,
505 line: Option<u32>,
506 col: Option<u32>,
507}
508
509fn build_symbol(
510 item: &RustdocItem,
511 options: &RustdocParseOptions,
512 parts: SymbolParts,
513 kind_override: Option<&str>,
514 parsed_docs: Option<&ParsedDocs>,
515) -> Symbol {
516 let SymbolParts {
517 name,
518 qualified_name,
519 symbol_key,
520 signature,
521 params,
522 return_type,
523 type_params,
524 source_path,
525 line,
526 col,
527 } = parts;
528
529 let name_value = if name.is_empty() {
530 None
531 } else {
532 Some(name)
533 };
534 let qualified_value = if qualified_name.is_empty() {
535 None
536 } else {
537 Some(qualified_name)
538 };
539
540 Symbol {
541 id: None,
542 project_id: options.project_id.clone(),
543 language: Some(options.language.clone()),
544 symbol_key,
545 kind: kind_override.map(str::to_string).or_else(|| inner_kind(item).map(str::to_string)),
546 name: name_value.clone(),
547 qualified_name: qualified_value,
548 display_name: name_value,
549 signature,
550 signature_hash: None,
551 visibility: item.visibility.clone(),
552 is_static: item_is_static(item),
553 is_async: item_is_async(item),
554 is_const: item_is_const(item),
555 is_deprecated: item.deprecation.is_some().then_some(true),
556 since: item.deprecation.as_ref().and_then(|dep| dep.since.clone()),
557 stability: None,
558 source_path,
559 line,
560 col,
561 return_type,
562 params,
563 type_params,
564 attributes: Vec::new(),
565 source_ids: vec![SourceId {
566 kind: "rustdoc_id".to_string(),
567 value: item.id.to_string(),
568 }],
569 doc_summary: parsed_docs.and_then(|docs| docs.summary.clone()),
570 extra: None,
571 }
572}
573
574fn build_doc_block(
575 options: &RustdocParseOptions,
576 symbol_key: String,
577 parsed_docs: ParsedDocs,
578 raw_docs: &str,
579) -> DocBlock {
580 DocBlock {
581 id: None,
582 project_id: options.project_id.clone(),
583 ingest_id: options.ingest_id.clone(),
584 symbol_key: Some(symbol_key),
585 language: Some(options.language.clone()),
586 source_kind: Some(options.source_kind.clone()),
587 doc_hash: None,
588 summary: parsed_docs.summary,
589 remarks: parsed_docs.remarks,
590 returns: parsed_docs.returns,
591 value: parsed_docs.value,
592 params: parsed_docs.params,
593 type_params: parsed_docs.type_params,
594 exceptions: Vec::new(),
595 examples: parsed_docs.examples,
596 notes: parsed_docs.notes,
597 warnings: parsed_docs.warnings,
598 safety: parsed_docs.safety,
599 panics: parsed_docs.panics,
600 errors: parsed_docs.errors,
601 see_also: parsed_docs.see_also,
602 deprecated: parsed_docs.deprecated,
603 inherit_doc: None,
604 sections: parsed_docs.sections,
605 raw: Some(raw_docs.to_string()),
606 extra: None,
607 }
608}
609
610#[derive(Debug)]
611struct ParsedDocs {
612 summary: Option<String>,
613 remarks: Option<String>,
614 returns: Option<String>,
615 value: Option<String>,
616 errors: Option<String>,
617 panics: Option<String>,
618 safety: Option<String>,
619 deprecated: Option<String>,
620 params: Vec<DocParam>,
621 type_params: Vec<DocTypeParam>,
622 examples: Vec<DocExample>,
623 notes: Vec<String>,
624 warnings: Vec<String>,
625 see_also: Vec<SeeAlso>,
626 sections: Vec<DocSection>,
627}
628
629fn build_id_path_map(crate_doc: &RustdocCrate, root_crate_id: u64) -> HashMap<u64, String> {
630 let mut map = HashMap::new();
631 for (id, path) in &crate_doc.paths {
632 if path.crate_id != root_crate_id {
633 continue;
634 }
635 if let Ok(parsed_id) = id.parse::<u64>() {
636 let joined = path.path.join("::");
637 map.insert(parsed_id, joined);
638 }
639 }
640 map
641}
642
643fn inner_kind(item: &RustdocItem) -> Option<&str> {
644 item.inner.keys().next().map(String::as_str)
645}
646
647fn is_inner_kind(item: &RustdocItem, kind: &str) -> bool {
648 matches!(inner_kind(item), Some(found) if found == kind)
649}
650
651fn module_items(item: &RustdocItem) -> Vec<u64> {
652 item.inner
653 .get("module")
654 .and_then(|value| value.get("items"))
655 .and_then(Value::as_array)
656 .map(|items| extract_ids(items))
657 .unwrap_or_default()
658}
659
660fn struct_kind_fields(kind: &Value) -> Vec<u64> {
661 if let Some(plain) = kind.get("plain") {
662 return plain
663 .get("fields")
664 .and_then(Value::as_array)
665 .map(|items| extract_ids(items))
666 .unwrap_or_default();
667 }
668 if let Some(tuple) = kind.get("tuple") {
669 return tuple
670 .get("fields")
671 .and_then(Value::as_array)
672 .map(|items| extract_ids(items))
673 .unwrap_or_default();
674 }
675 Vec::new()
676}
677
678fn extract_ids(items: &[Value]) -> Vec<u64> {
679 items.iter().filter_map(Value::as_u64).collect()
680}
681
682fn parse_signature(
683 item: &RustdocItem,
684 state: &ParserState<'_>,
685 name: &str,
686) -> (Vec<Param>, Option<TypeRef>, Option<String>) {
687 let Some(inner) = item.inner.get("function") else {
688 let return_type = match inner_kind(item) {
689 Some("constant") => item
690 .inner
691 .get("constant")
692 .and_then(|value| value.get("type"))
693 .map(|ty| type_to_ref(ty, state)),
694 Some("static") => item
695 .inner
696 .get("static")
697 .and_then(|value| value.get("type"))
698 .map(|ty| type_to_ref(ty, state)),
699 Some("struct_field") => item
700 .inner
701 .get("struct_field")
702 .map(|ty| type_to_ref(ty, state)),
703 Some("type_alias") => item
704 .inner
705 .get("type_alias")
706 .and_then(|value| value.get("type"))
707 .map(|ty| type_to_ref(ty, state)),
708 _ => None,
709 };
710 return (Vec::new(), return_type, None);
711 };
712
713 let Some(sig) = inner.get("sig") else {
714 return (Vec::new(), None, None);
715 };
716
717 let mut params = Vec::new();
718 if let Some(inputs) = sig.get("inputs").and_then(Value::as_array) {
719 for input in inputs {
720 let Some(pair) = input.as_array() else {
721 continue;
722 };
723 if pair.len() != 2 {
724 continue;
725 }
726 let name = pair[0].as_str().unwrap_or("").to_string();
727 let ty = type_to_ref(&pair[1], state);
728 params.push(Param {
729 name,
730 type_ref: Some(ty),
731 default_value: None,
732 is_optional: None,
733 });
734 }
735 }
736
737 let return_type = sig
738 .get("output")
739 .and_then(|output| {
740 if output.is_null() {
741 None
742 } else {
743 Some(type_to_ref(output, state))
744 }
745 });
746
747 let signature = format_function_signature(name, ¶ms, return_type.as_ref());
748 (params, return_type, Some(signature))
749}
750
751fn parse_type_params(item: &RustdocItem) -> Vec<TypeParam> {
752 let Some(kind) = inner_kind(item) else {
753 return Vec::new();
754 };
755 let generics = match kind {
756 "function" => item
757 .inner
758 .get("function")
759 .and_then(|value| value.get("generics")),
760 "struct" => item
761 .inner
762 .get("struct")
763 .and_then(|value| value.get("generics")),
764 "enum" => item.inner.get("enum").and_then(|value| value.get("generics")),
765 "trait" => item.inner.get("trait").and_then(|value| value.get("generics")),
766 "type_alias" => item
767 .inner
768 .get("type_alias")
769 .and_then(|value| value.get("generics")),
770 _ => None,
771 };
772
773 let Some(generics) = generics else {
774 return Vec::new();
775 };
776 let Some(params) = generics.get("params").and_then(Value::as_array) else {
777 return Vec::new();
778 };
779
780 let mut output = Vec::new();
781 for param in params {
782 let Some(name) = param.get("name").and_then(Value::as_str) else {
783 continue;
784 };
785 let mut constraints = Vec::new();
786 if let Some(bounds) = param
787 .get("kind")
788 .and_then(|kind| kind.get("type"))
789 .and_then(|type_info| type_info.get("bounds"))
790 .and_then(Value::as_array)
791 {
792 for bound in bounds {
793 if let Some(path) = bound
794 .get("trait_bound")
795 .and_then(|trait_bound| trait_bound.get("trait"))
796 .and_then(|trait_path| trait_path.get("path"))
797 .and_then(Value::as_str)
798 {
799 constraints.push(path.to_string());
800 }
801 }
802 }
803 output.push(TypeParam {
804 name: name.to_string(),
805 constraints,
806 });
807 }
808 output
809}
810
811fn item_is_async(item: &RustdocItem) -> Option<bool> {
812 item.inner
813 .get("function")
814 .and_then(|value| value.get("header"))
815 .and_then(|value| value.get("is_async"))
816 .and_then(Value::as_bool)
817 .filter(|is_async| *is_async)
818 .map(|_| true)
819}
820
821fn item_is_const(item: &RustdocItem) -> Option<bool> {
822 if matches!(inner_kind(item), Some("constant")) {
823 return Some(true);
824 }
825 item.inner
826 .get("function")
827 .and_then(|value| value.get("header"))
828 .and_then(|value| value.get("is_const"))
829 .and_then(Value::as_bool)
830 .filter(|is_const| *is_const)
831 .map(|_| true)
832}
833
834fn item_is_static(item: &RustdocItem) -> Option<bool> {
835 matches!(inner_kind(item), Some("static")).then_some(true)
836}
837fn format_function_signature(
838 name: &str,
839 params: &[Param],
840 output: Option<&TypeRef>,
841) -> String {
842 let params = params
843 .iter()
844 .map(|param| match param.type_ref.as_ref().and_then(|ty| ty.display.as_ref()) {
845 Some(ty) if !param.name.is_empty() => format!("{}: {ty}", param.name),
846 Some(ty) => ty.clone(),
847 None => param.name.clone(),
848 })
849 .collect::<Vec<_>>()
850 .join(", ");
851 let mut sig = format!("fn {name}({params})");
852 if let Some(output) = output.and_then(|ty| ty.display.as_ref()) && output != "()" {
853 sig.push_str(" -> ");
854 sig.push_str(output);
855 }
856 sig
857}
858
859fn type_to_ref(value: &Value, state: &ParserState<'_>) -> TypeRef {
860 let display = type_to_string(value, state).unwrap_or_else(|| "<unknown>".to_string());
861 let symbol_key = type_symbol_key(value, state);
862 TypeRef {
863 display: Some(display.clone()),
864 canonical: Some(display),
865 language: Some(state.options.language.clone()),
866 symbol_key,
867 generics: Vec::new(),
868 modifiers: Vec::new(),
869 }
870}
871
872fn type_symbol_key(value: &Value, state: &ParserState<'_>) -> Option<String> {
873 let resolved = value.get("resolved_path")?;
874 let id = resolved.get("id").and_then(Value::as_u64)?;
875 let path = state.id_to_path.get(&id)?.clone();
876 Some(make_symbol_key("rust", &state.options.project_id, &path))
877}
878
879fn type_to_string(value: &Value, state: &ParserState<'_>) -> Option<String> {
880 primitive_type(value)
881 .or_else(|| generic_type(value))
882 .or_else(|| resolved_path_type(value, state))
883 .or_else(|| borrowed_ref_type(value, state))
884 .or_else(|| raw_pointer_type(value, state))
885 .or_else(|| tuple_type(value, state))
886 .or_else(|| slice_type(value, state))
887 .or_else(|| array_type(value, state))
888 .or_else(|| impl_trait_type(value, state))
889 .or_else(|| dyn_trait_type(value, state))
890 .or_else(|| qualified_path_type(value, state))
891 .or_else(|| function_pointer_type(value, state))
892}
893
894fn primitive_type(value: &Value) -> Option<String> {
895 value
896 .get("primitive")
897 .and_then(Value::as_str)
898 .map(str::to_string)
899}
900
901fn generic_type(value: &Value) -> Option<String> {
902 value
903 .get("generic")
904 .and_then(Value::as_str)
905 .map(str::to_string)
906}
907
908fn resolved_path_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
909 let resolved = value.get("resolved_path")?;
910 let path = resolved.get("path").and_then(Value::as_str)?;
911 let args = resolved.get("args");
912 Some(format!("{}{}", path, format_type_args(args, state)))
913}
914
915fn borrowed_ref_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
916 let borrowed = value.get("borrowed_ref")?;
917 let is_mut = borrowed
918 .get("is_mutable")
919 .and_then(Value::as_bool)
920 .unwrap_or(false);
921 let inner = borrowed.get("type").and_then(|inner| type_to_string(inner, state))?;
922 Some(if is_mut {
923 format!("&mut {inner}")
924 } else {
925 format!("&{inner}")
926 })
927}
928
929fn raw_pointer_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
930 let raw = value.get("raw_pointer")?;
931 let is_mut = raw
932 .get("is_mutable")
933 .and_then(Value::as_bool)
934 .unwrap_or(false);
935 let inner = raw.get("type").and_then(|inner| type_to_string(inner, state))?;
936 Some(if is_mut {
937 format!("*mut {inner}")
938 } else {
939 format!("*const {inner}")
940 })
941}
942
943fn tuple_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
944 let tuple = value.get("tuple").and_then(Value::as_array)?;
945 let parts = tuple
946 .iter()
947 .filter_map(|inner| type_to_string(inner, state))
948 .collect::<Vec<_>>()
949 .join(", ");
950 Some(format!("({parts})"))
951}
952
953fn slice_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
954 let slice = value.get("slice")?;
955 let inner = type_to_string(slice, state)?;
956 Some(format!("[{inner}]"))
957}
958
959fn array_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
960 let array = value.get("array")?;
961 let inner = array.get("type").and_then(|inner| type_to_string(inner, state))?;
962 let len = array.get("len").and_then(Value::as_str).unwrap_or("");
963 if len.is_empty() {
964 Some(format!("[{inner}]"))
965 } else {
966 Some(format!("[{inner}; {len}]"))
967 }
968}
969
970fn impl_trait_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
971 let impl_trait = value.get("impl_trait").and_then(Value::as_array)?;
972 let bounds = impl_trait
973 .iter()
974 .filter_map(|bound| trait_bound_to_string(bound, state))
975 .collect::<Vec<_>>()
976 .join(" + ");
977 if bounds.is_empty() {
978 Some("impl".to_string())
979 } else {
980 Some(format!("impl {bounds}"))
981 }
982}
983
984fn dyn_trait_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
985 let dyn_trait = value.get("dyn_trait")?;
986 let traits = dyn_trait
987 .get("traits")
988 .and_then(Value::as_array)
989 .map(|items| {
990 items
991 .iter()
992 .filter_map(|bound| trait_bound_to_string(bound, state))
993 .collect::<Vec<_>>()
994 .join(" + ")
995 })
996 .unwrap_or_default();
997 if traits.is_empty() {
998 Some("dyn".to_string())
999 } else {
1000 Some(format!("dyn {traits}"))
1001 }
1002}
1003
1004fn qualified_path_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
1005 let qualified = value.get("qualified_path")?;
1006 let name = qualified.get("name").and_then(Value::as_str).unwrap_or("");
1007 let self_type = qualified
1008 .get("self_type")
1009 .and_then(|inner| type_to_string(inner, state))
1010 .unwrap_or_default();
1011 let trait_name = qualified
1012 .get("trait")
1013 .and_then(|inner| inner.get("path"))
1014 .and_then(Value::as_str)
1015 .unwrap_or("");
1016 if !trait_name.is_empty() {
1017 Some(format!("<{self_type} as {trait_name}>::{name}"))
1018 } else if !self_type.is_empty() {
1019 Some(format!("{self_type}::{name}"))
1020 } else {
1021 None
1022 }
1023}
1024
1025fn function_pointer_type(value: &Value, state: &ParserState<'_>) -> Option<String> {
1026 let fn_pointer = value.get("function_pointer")?;
1027 let decl = fn_pointer.get("decl")?;
1028 let params = decl
1029 .get("inputs")
1030 .and_then(Value::as_array)
1031 .map(|inputs| {
1032 inputs
1033 .iter()
1034 .filter_map(|pair| pair.as_array())
1035 .filter_map(|pair| pair.get(1))
1036 .filter_map(|param| type_to_string(param, state))
1037 .collect::<Vec<_>>()
1038 .join(", ")
1039 })
1040 .unwrap_or_default();
1041 let output = decl
1042 .get("output")
1043 .and_then(|output| type_to_string(output, state))
1044 .unwrap_or_else(|| "()".to_string());
1045 Some(format!("fn({params}) -> {output}"))
1046}
1047
1048fn format_type_args(args: Option<&Value>, state: &ParserState<'_>) -> String {
1049 let Some(args) = args else {
1050 return String::new();
1051 };
1052 let Some(angle) = args.get("angle_bracketed") else {
1053 return String::new();
1054 };
1055 let Some(items) = angle.get("args").and_then(Value::as_array) else {
1056 return String::new();
1057 };
1058 let mut rendered = Vec::new();
1059 for item in items {
1060 if let Some(ty) = item.get("type").and_then(|inner| type_to_string(inner, state)) {
1061 rendered.push(ty);
1062 } else if let Some(lifetime) = item.get("lifetime").and_then(Value::as_str) {
1063 rendered.push(lifetime.to_string());
1064 } else if let Some(const_val) = item.get("const").and_then(Value::as_str) {
1065 rendered.push(const_val.to_string());
1066 }
1067 }
1068 if rendered.is_empty() {
1069 String::new()
1070 } else {
1071 format!("<{}>", rendered.join(", "))
1072 }
1073}
1074
1075fn trait_bound_to_string(value: &Value, state: &ParserState<'_>) -> Option<String> {
1076 let trait_bound = value.get("trait_bound")?;
1077 let trait_path = trait_bound.get("trait")?;
1078 let path = trait_path.get("path").and_then(Value::as_str)?;
1079 let args = trait_path.get("args");
1080 Some(format!("{}{}", path, format_type_args(args, state)))
1081}
1082fn parse_markdown_docs(raw: &str) -> ParsedDocs {
1083 let normalized = raw.replace("\r\n", "\n");
1084 let (preamble, sections) = split_sections(&normalized);
1085 let (summary, remarks) = split_summary_remarks(&preamble);
1086
1087 let mut parsed = ParsedDocs {
1088 summary,
1089 remarks,
1090 returns: None,
1091 value: None,
1092 errors: None,
1093 panics: None,
1094 safety: None,
1095 deprecated: None,
1096 params: Vec::new(),
1097 type_params: Vec::new(),
1098 examples: Vec::new(),
1099 notes: Vec::new(),
1100 warnings: Vec::new(),
1101 see_also: Vec::new(),
1102 sections: Vec::new(),
1103 };
1104
1105 for (title, body) in sections {
1106 let normalized_title = title.trim().to_ascii_lowercase();
1107 let trimmed_body = body.trim();
1108 if trimmed_body.is_empty() {
1109 continue;
1110 }
1111 match normalized_title.as_str() {
1112 "errors" => parsed.errors = Some(trimmed_body.to_string()),
1113 "panics" => parsed.panics = Some(trimmed_body.to_string()),
1114 "safety" => parsed.safety = Some(trimmed_body.to_string()),
1115 "returns" => parsed.returns = Some(trimmed_body.to_string()),
1116 "value" => parsed.value = Some(trimmed_body.to_string()),
1117 "deprecated" => parsed.deprecated = Some(trimmed_body.to_string()),
1118 "examples" | "example" => parsed.examples = extract_examples(trimmed_body),
1119 "notes" | "note" => parsed.notes.push(trimmed_body.to_string()),
1120 "warnings" | "warning" => parsed.warnings.push(trimmed_body.to_string()),
1121 "see also" | "seealso" | "see-also" => {
1122 parsed.see_also = parse_see_also_section(trimmed_body);
1123 }
1124 "arguments" | "args" | "parameters" | "params" => {
1125 parsed.params = parse_param_section(trimmed_body);
1126 }
1127 "type parameters" | "type params" | "typeparam" | "typeparams" => {
1128 parsed.type_params = parse_type_param_section(trimmed_body);
1129 }
1130 _ => parsed.sections.push(DocSection {
1131 title,
1132 body: trimmed_body.to_string(),
1133 }),
1134 }
1135 }
1136
1137 parsed
1138}
1139
1140fn parse_see_also_section(body: &str) -> Vec<SeeAlso> {
1141 let mut entries = Vec::new();
1142 for line in body.lines() {
1143 let trimmed = line.trim();
1144 if trimmed.is_empty() {
1145 continue;
1146 }
1147 let item = trimmed
1148 .strip_prefix("- ")
1149 .or_else(|| trimmed.strip_prefix("* "))
1150 .unwrap_or(trimmed);
1151 if let Some(see) = parse_see_also_line(item) {
1152 entries.push(see);
1153 }
1154 }
1155 if entries.is_empty()
1156 && let Some(see) = parse_see_also_line(body.trim())
1157 {
1158 entries.push(see);
1159 }
1160 entries
1161}
1162
1163fn parse_see_also_line(text: &str) -> Option<SeeAlso> {
1164 let trimmed = text.trim();
1165 if trimmed.is_empty() {
1166 return None;
1167 }
1168 if let Some((label, target)) = parse_markdown_link(trimmed) {
1169 return Some(SeeAlso {
1170 label: Some(label),
1171 target,
1172 target_kind: Some("markdown".to_string()),
1173 });
1174 }
1175 Some(SeeAlso {
1176 label: None,
1177 target: trimmed.to_string(),
1178 target_kind: Some("text".to_string()),
1179 })
1180}
1181
1182fn parse_markdown_link(text: &str) -> Option<(String, String)> {
1183 let start = text.find('[')?;
1184 let remainder = &text[start + 1..];
1185 let mid = remainder.find("](")?;
1186 let label = remainder[..mid].trim();
1187 let tail = &remainder[mid + 2..];
1188 let end = tail.find(')')?;
1189 let target = tail[..end].trim();
1190 if label.is_empty() || target.is_empty() {
1191 return None;
1192 }
1193 Some((label.to_string(), target.to_string()))
1194}
1195
1196fn split_sections(doc: &str) -> (String, Vec<(String, String)>) {
1197 let mut preamble = Vec::new();
1198 let mut sections = Vec::new();
1199 let mut current_title: Option<String> = None;
1200 let mut current_body = Vec::new();
1201 let mut in_code = false;
1202
1203 for line in doc.lines() {
1204 let trimmed = line.trim_start();
1205 if trimmed.starts_with("```") {
1206 in_code = !in_code;
1207 if current_title.is_some() {
1208 current_body.push(line.to_string());
1209 } else {
1210 preamble.push(line.to_string());
1211 }
1212 continue;
1213 }
1214 if !in_code && let Some(title) = parse_heading(trimmed) {
1215 if let Some(active) = current_title.take() {
1216 sections.push((active, current_body.join("\n").trim().to_string()));
1217 current_body.clear();
1218 }
1219 current_title = Some(title);
1220 continue;
1221 }
1222 if current_title.is_some() {
1223 current_body.push(line.to_string());
1224 } else {
1225 preamble.push(line.to_string());
1226 }
1227 }
1228
1229 if let Some(active) = current_title.take() {
1230 sections.push((active, current_body.join("\n").trim().to_string()));
1231 }
1232
1233 (preamble.join("\n").trim().to_string(), sections)
1234}
1235
1236fn parse_heading(line: &str) -> Option<String> {
1237 let trimmed = line.trim();
1238 if !trimmed.starts_with('#') {
1239 return None;
1240 }
1241 let hash_count = trimmed.chars().take_while(|ch| *ch == '#').count();
1242 if hash_count == 0 {
1243 return None;
1244 }
1245 let rest = trimmed[hash_count..].trim_start();
1246 if rest.is_empty() {
1247 None
1248 } else {
1249 Some(rest.to_string())
1250 }
1251}
1252
1253fn split_summary_remarks(preamble: &str) -> (Option<String>, Option<String>) {
1254 let mut paragraphs = preamble
1255 .split("\n\n")
1256 .map(str::trim)
1257 .filter(|part| !part.is_empty());
1258 let summary = paragraphs.next().map(str::to_string);
1259 let rest = paragraphs.collect::<Vec<_>>().join("\n\n");
1260 let remarks = if rest.is_empty() {
1261 None
1262 } else {
1263 Some(rest)
1264 };
1265 (summary, remarks)
1266}
1267
1268fn extract_examples(body: &str) -> Vec<DocExample> {
1269 let mut examples = Vec::new();
1270 let mut in_code = false;
1271 let mut current_lang: Option<String> = None;
1272 let mut current_code = Vec::new();
1273
1274 for line in body.lines() {
1275 let trimmed = line.trim_start();
1276 if trimmed.starts_with("```") {
1277 if in_code {
1278 let code = current_code.join("\n");
1279 if !code.trim().is_empty() {
1280 examples.push(DocExample {
1281 lang: current_lang.take(),
1282 code: Some(code),
1283 caption: None,
1284 });
1285 }
1286 current_code.clear();
1287 in_code = false;
1288 } else {
1289 let lang = trimmed.trim_start_matches("```").trim();
1290 current_lang = if lang.is_empty() {
1291 None
1292 } else {
1293 Some(lang.to_string())
1294 };
1295 in_code = true;
1296 }
1297 continue;
1298 }
1299 if in_code {
1300 current_code.push(line.to_string());
1301 }
1302 }
1303
1304 if !examples.is_empty() {
1305 return examples;
1306 }
1307 let trimmed = body.trim();
1308 if trimmed.is_empty() {
1309 Vec::new()
1310 } else {
1311 vec![DocExample {
1312 lang: None,
1313 code: Some(trimmed.to_string()),
1314 caption: None,
1315 }]
1316 }
1317}
1318
1319fn parse_param_section(body: &str) -> Vec<DocParam> {
1320 let mut params = Vec::new();
1321 for line in body.lines() {
1322 let trimmed = line.trim();
1323 if !(trimmed.starts_with('-') || trimmed.starts_with('*')) {
1324 continue;
1325 }
1326 let item = trimmed.trim_start_matches(['-', '*']).trim();
1327 if item.is_empty() {
1328 continue;
1329 }
1330 if let Some((name, description)) = split_param_item(item) {
1331 params.push(DocParam {
1332 name,
1333 description,
1334 type_ref: None,
1335 });
1336 }
1337 }
1338 params
1339}
1340
1341fn parse_type_param_section(body: &str) -> Vec<DocTypeParam> {
1342 let mut params = Vec::new();
1343 for line in body.lines() {
1344 let trimmed = line.trim();
1345 if !(trimmed.starts_with('-') || trimmed.starts_with('*')) {
1346 continue;
1347 }
1348 let item = trimmed.trim_start_matches(['-', '*']).trim();
1349 if item.is_empty() {
1350 continue;
1351 }
1352 if let Some((name, description)) = split_param_item(item) {
1353 params.push(DocTypeParam { name, description });
1354 }
1355 }
1356 params
1357}
1358
1359fn split_param_item(item: &str) -> Option<(String, Option<String>)> {
1360 let (name, description) = if let Some((name, rest)) = item.split_once(':') {
1361 (name, Some(rest))
1362 } else if let Some((name, rest)) = item.split_once(" - ") {
1363 (name, Some(rest))
1364 } else {
1365 (item, None)
1366 };
1367
1368 let name = name.trim().trim_matches('`');
1369 if name.is_empty() {
1370 return None;
1371 }
1372 let description = description.map(|rest| rest.trim().to_string()).filter(|s| !s.is_empty());
1373 Some((name.to_string(), description))
1374}
1375
1376#[cfg(test)]
1377mod tests {
1378 use super::parse_markdown_docs;
1379
1380 #[test]
1381 fn parse_markdown_docs_extracts_see_also() {
1382 let docs = "Summary.\n\n# See Also\n- [Foo](crate::Foo)\n- Bar";
1383 let parsed = parse_markdown_docs(docs);
1384
1385 assert_eq!(parsed.see_also.len(), 2);
1386 assert_eq!(parsed.see_also[0].label.as_deref(), Some("Foo"));
1387 assert_eq!(parsed.see_also[0].target, "crate::Foo");
1388 assert_eq!(parsed.see_also[1].label.as_deref(), None);
1389 assert_eq!(parsed.see_also[1].target, "Bar");
1390 }
1391}