Skip to main content

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}