//! The item inventory, read out of rustdoc's JSON output. //! //! rustdoc knows each item's kind, its visibility, the module path it is //! reached by and the source span it occupies. LSIF knows none of that well, //! which is why the index is built from both. use crate::layout::Layout; use anyhow::{Context, Result}; use serde_json::Value; use std::collections::HashSet; use std::path::PathBuf; pub struct Item { pub krate: String, pub module_path: String, pub name: String, pub kind: String, pub visibility: String, pub file: String, pub line_start: u32, pub line_end: u32, pub target_kind: String, /// Every module between the crate root and this item is `pub`, so the /// item is part of the crate's published API rather than merely spelled /// `pub` inside a private module. pub exported: bool, } /// Reads every `*.json` in each rustdoc JSON output directory. /// /// More than one directory, because rustdoc names its output after the crate /// and a package's lib and its bin often share a name: documenting the two /// into one directory loses whichever ran first. pub fn load(dirs: &[PathBuf], layout: &Layout) -> Result> { let mut items = Vec::new(); let mut entries: Vec<_> = Vec::new(); for dir in dirs { entries.extend( std::fs::read_dir(dir) .with_context(|| format!("reading {}", dir.display()))? .filter_map(|e| e.ok()) .map(|e| e.path()) .filter(|p| p.extension().is_some_and(|e| e == "json")), ); } entries.sort(); for path in entries { let text = std::fs::read_to_string(&path) .with_context(|| format!("reading {}", path.display()))?; let doc: Value = serde_json::from_str(&text).with_context(|| format!("parsing {}", path.display()))?; walk_crate(&doc, layout, &mut items); } items.sort_by(|a, b| (&a.file, a.line_start, &a.name).cmp(&(&b.file, b.line_start, &b.name))); items.dedup_by(|a, b| (&a.file, a.line_start, &a.name) == (&b.file, b.line_start, &b.name)); Ok(items) } fn walk_crate(doc: &Value, layout: &Layout, out: &mut Vec) { let index = match doc["index"].as_object() { Some(i) => i, None => return, }; let root = doc["root"].to_string().trim_matches('"').to_string(); let mut seen = HashSet::new(); let mut stack = vec![(root, Vec::::new(), true)]; while let Some((id, prefix, public_chain)) = stack.pop() { if !seen.insert(id.clone()) { continue; } let item = match index.get(&id) { Some(i) => i, None => continue, }; let kind = kind_of(item); // A `use` carries no name of its own; the name it binds sits in the // re-export body, and its span is exactly that name's spelling. let name = item["name"] .as_str() .or_else(|| item["inner"]["use"]["name"].as_str()) .unwrap_or_default() .to_string(); let visibility = visibility_of(item); let public = matches!(visibility.as_str(), "public" | "default"); if let Some(span) = item["span"].as_object() { let file = span["filename"].as_str().unwrap_or_default(); if file.starts_with("crates/") && kind != "impl" && !name.is_empty() { if let Some((krate, target_kind)) = layout.locate(file) { out.push(Item { krate: krate.to_string(), module_path: prefix.join("::"), name: name.clone(), kind: kind.clone(), visibility: visibility.clone(), file: file.to_string(), line_start: span["begin"][0].as_u64().unwrap_or(0) as u32, line_end: span["end"][0].as_u64().unwrap_or(0) as u32, target_kind: target_kind.to_string(), exported: public_chain && visibility == "public", }); } } } // An impl block is already filed under the type it is written for, so // it adds nothing to the path; everything else that can hold items // does. A `use` binds a name without opening a scope. let child_prefix = if kind == "impl" || kind == "use" || name.is_empty() { prefix } else { let mut p = prefix; p.push(name); p }; let child_chain = public_chain && public; for child in children(item) { stack.push((child, child_prefix.clone(), child_chain)); } } } fn kind_of(item: &Value) -> String { item["inner"] .as_object() .and_then(|o| o.keys().next()) .cloned() .unwrap_or_else(|| "unknown".to_string()) } fn visibility_of(item: &Value) -> String { match &item["visibility"] { Value::String(s) => s.clone(), Value::Object(_) => "restricted".to_string(), _ => "default".to_string(), } } /// Every child id an item can carry, whatever shape rustdoc gives it. fn children(item: &Value) -> Vec { let mut out = Vec::new(); let Some(inner) = item["inner"].as_object() else { return out; }; for body in inner.values() { for key in ["items", "impls", "variants", "fields"] { push_ids(&body[key], &mut out); } // Struct and enum-variant payloads nest their field list one level // further, under the shape of the thing. if let Some(shape) = body["kind"].as_object() { for form in shape.values() { push_ids(&form["fields"], &mut out); } } } out } fn push_ids(value: &Value, out: &mut Vec) { if let Some(list) = value.as_array() { for id in list { if let Some(n) = id.as_u64() { out.push(n.to_string()); } } } }