aidoc_core/index.rs
1//! Indexed representation of a Rust workspace's public API surface.
2//!
3//! The index stage transforms rustdoc JSON (one file per crate) into these
4//! in-memory types. The generate stage consumes an [`IndexedWorkspace`] to
5//! emit `llms.txt`, narrative markdown, and machine JSON artifacts. The lint
6//! stage inspects the same structure to report doc-coverage violations.
7//!
8//! The entry point is [`IndexedWorkspace::build`], which walks the
9//! workspace via `cargo_metadata` and invokes rustdoc once per crate.
10
11use std::path::{Path, PathBuf};
12
13use cargo_metadata::{MetadataCommand, TargetKind};
14
15use crate::config::Config;
16use crate::error::Result;
17use crate::rustdoc::{self, Target};
18
19/// A single crate that has been indexed via rustdoc JSON.
20#[derive(Debug, Clone)]
21pub struct IndexedCrate {
22 /// The crate's package name (as reported by cargo metadata).
23 pub name: String,
24
25 /// The crate's version string (as reported by cargo metadata).
26 pub version: String,
27
28 /// The crate root's outer doc comment (`//!` block), if any. Sourced
29 /// from `rustdoc_types::Crate::index[root].docs`.
30 pub root_module_doc: Option<String>,
31
32 /// The raw parsed rustdoc JSON payload. Retained so the generate and
33 /// lint stages can walk item / module trees without re-parsing.
34 pub crate_data: rustdoc_types::Crate,
35}
36
37/// The complete indexed workspace: every crate that was successfully
38/// indexed, plus workspace-level context needed by the generate stage.
39#[derive(Debug, Clone)]
40pub struct IndexedWorkspace {
41 /// Workspace root directory (parent of the top-level `Cargo.toml`).
42 pub root: PathBuf,
43
44 /// Indexed crates in the order they were discovered by cargo metadata.
45 /// Callers should not assume alphabetic or dependency order.
46 pub crates: Vec<IndexedCrate>,
47}
48
49impl IndexedWorkspace {
50 /// Enumerate the workspace at `workspace_root`, run rustdoc for every
51 /// crate that is not excluded by `config`, and package the parsed
52 /// results into an [`IndexedWorkspace`].
53 ///
54 /// Crates that expose neither a library nor a binary target are
55 /// skipped silently (e.g. workspace-only virtual manifests). A single
56 /// crate failure aborts the whole index pass; there is no partial
57 /// result path at this stage.
58 pub fn build(workspace_root: &Path, config: &Config) -> Result<Self> {
59 let metadata = MetadataCommand::new()
60 .manifest_path(workspace_root.join("Cargo.toml"))
61 .exec()?;
62
63 let root: PathBuf = metadata.workspace_root.as_std_path().to_path_buf();
64 let mut crates = Vec::new();
65
66 for package in metadata.workspace_packages() {
67 let package_name = package.name.to_string();
68
69 if config
70 .exclude
71 .iter()
72 .any(|excluded| excluded == &package_name)
73 {
74 continue;
75 }
76
77 let Some(target) = select_target(&package.targets) else {
78 continue;
79 };
80
81 let manifest_path = package.manifest_path.as_std_path();
82 let crate_data =
83 rustdoc::build_and_parse(&target, &package_name, manifest_path, &root)?;
84
85 let root_module_doc = crate_data
86 .index
87 .get(&crate_data.root)
88 .and_then(|item| item.docs.clone());
89
90 crates.push(IndexedCrate {
91 name: package_name,
92 version: package.version.to_string(),
93 root_module_doc,
94 crate_data,
95 });
96 }
97
98 Ok(IndexedWorkspace { root, crates })
99 }
100}
101
102/// Pick which rustdoc target to build for a crate.
103///
104/// Prefers the library target; falls back to the first binary target.
105/// Proc-macro / example / test / bench targets are ignored because they
106/// don't contribute to the crate's public API surface.
107fn select_target(targets: &[cargo_metadata::Target]) -> Option<Target<'_>> {
108 if targets.iter().any(|t| {
109 t.kind
110 .iter()
111 .any(|k| matches!(k, TargetKind::Lib | TargetKind::RLib))
112 }) {
113 return Some(Target::Lib);
114 }
115
116 targets.iter().find_map(|t| {
117 if t.kind.iter().any(|k| matches!(k, TargetKind::Bin)) {
118 Some(Target::Bin(t.name.as_str()))
119 } else {
120 None
121 }
122 })
123}