Skip to main content

docx_core/parsers/
rustdoc_json.rs

1//! Rustdoc JSON parser.
2
3use 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/// Options for parsing rustdoc JSON.
24#[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/// Output from parsing rustdoc JSON.
50#[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/// Error type for rustdoc JSON parse failures.
58#[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
97/// Parser for rustdoc JSON output.
98pub struct RustdocJsonParser;
99
100impl RustdocJsonParser {
101    /// Parses rustdoc JSON into symbols and doc blocks.
102    ///
103    /// # Errors
104    /// Returns `RustdocParseError` if the JSON is invalid or cannot be parsed.
105    #[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    /// Parses rustdoc JSON asynchronously using a blocking task.
144    ///
145    /// # Errors
146    /// Returns `RustdocParseError` if parsing fails or the task panics.
147    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    /// Parses rustdoc JSON from a file path asynchronously.
155    ///
156    /// # Errors
157    /// Returns `RustdocParseError` if the file cannot be read or the JSON cannot be parsed.
158    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, &params, 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}