crate_api/
rustdoc.rs

1use std::collections::HashMap;
2use std::collections::VecDeque;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct RustDocBuilder {
6    deps: bool,
7    target_directory: Option<std::path::PathBuf>,
8    silence: bool,
9    color: Option<bool>,
10}
11
12impl RustDocBuilder {
13    pub fn new() -> Self {
14        Self {
15            deps: false,
16            target_directory: None,
17            silence: false,
18            color: None,
19        }
20    }
21
22    /// Include dependencies
23    ///
24    /// Reasons to have this disabled:
25    /// - Faster API extraction
26    /// - Less likely to hit bugs in rustdoc, like
27    ///   - rust-lang/rust#89097
28    ///   - rust-lang/rust#83718
29    ///
30    /// Reasons to have this enabled:
31    /// - Check for accidental inclusion of dependencies in your API
32    /// - Detect breaking changes from dependencies in your API
33    pub fn deps(mut self, yes: bool) -> Self {
34        self.deps = yes;
35        self
36    }
37
38    pub fn target_directory(mut self, path: impl Into<std::path::PathBuf>) -> Self {
39        self.target_directory = Some(path.into());
40        self
41    }
42
43    /// Don't write progress to stderr
44    pub fn silence(mut self, yes: bool) -> Self {
45        self.silence = yes;
46        self
47    }
48
49    /// Whether stderr can be colored
50    pub fn color(mut self, yes: impl Into<Option<bool>>) -> Self {
51        self.color = yes.into();
52        self
53    }
54
55    pub fn dump_raw(self, manifest_path: &std::path::Path) -> Result<String, crate::Error> {
56        let manifest = std::fs::read_to_string(manifest_path).map_err(|e| {
57            crate::Error::new(
58                crate::ErrorKind::ApiParse,
59                format!("Failed when reading {}: {}", manifest_path.display(), e),
60            )
61        })?;
62        let manifest: toml_edit::Document = manifest.parse().map_err(|e| {
63            crate::Error::new(
64                crate::ErrorKind::ApiParse,
65                format!("Failed to parse {}: {}", manifest_path.display(), e),
66            )
67        })?;
68        let crate_name = manifest["package"]["name"].as_str().ok_or_else(|| {
69            crate::Error::new(
70                crate::ErrorKind::ApiParse,
71                format!(
72                    "Failed to parse {}: invalid package.name",
73                    manifest_path.display()
74                ),
75            )
76        })?;
77
78        let manifest_target_directory;
79        let target_dir = if let Some(target_dir) = self.target_directory.as_deref() {
80            target_dir
81        } else {
82            let metadata = cargo_metadata::MetadataCommand::new()
83                .manifest_path(manifest_path)
84                .no_deps()
85                .exec()
86                .map_err(|e| crate::Error::new(crate::ErrorKind::ApiParse, e))?;
87            manifest_target_directory = metadata
88                .target_directory
89                .as_path()
90                .as_std_path()
91                // HACK: Avoid potential errors when mixing toolchains
92                .join("crate-api/target");
93            manifest_target_directory.as_path()
94        };
95
96        let stderr = if self.silence {
97            std::process::Stdio::piped()
98        } else {
99            // Print cargo doc progress
100            std::process::Stdio::inherit()
101        };
102
103        let mut cmd = std::process::Command::new("cargo");
104        cmd.env(
105            "RUSTDOCFLAGS",
106            "-Z unstable-options --document-hidden-items --output-format=json",
107        )
108        .stdout(std::process::Stdio::null()) // Don't pollute cargo api output
109        .stderr(stderr)
110        .args(["+nightly", "doc", "--all-features"])
111        .arg("--manifest-path")
112        .arg(manifest_path)
113        .arg("--target-dir")
114        .arg(target_dir);
115        if !self.deps {
116            cmd.arg("--no-deps");
117        }
118        if let Some(color) = self.color {
119            if color {
120                cmd.arg("--color=always");
121            } else {
122                cmd.arg("--color=never");
123            }
124        }
125
126        let output = cmd
127            .output()
128            .map_err(|e| crate::Error::new(crate::ErrorKind::ApiParse, e))?;
129        if !output.status.success() {
130            let message = if self.silence {
131                format!(
132                    "Failed when running cargo-doc on {}: {}",
133                    manifest_path.display(),
134                    String::from_utf8_lossy(&output.stderr)
135                )
136            } else {
137                format!(
138                    "Failed when running cargo-doc on {}. See stderr.",
139                    manifest_path.display(),
140                )
141            };
142            return Err(crate::Error::new(crate::ErrorKind::ApiParse, message));
143        }
144
145        let json_path = target_dir.join(format!("doc/{}.json", crate_name));
146        std::fs::read_to_string(&json_path).map_err(|e| {
147            crate::Error::new(
148                crate::ErrorKind::ApiParse,
149                format!("Failed when loading {}: {}", json_path.display(), e),
150            )
151        })
152    }
153
154    pub fn into_api(self, manifest_path: &std::path::Path) -> Result<crate::Api, crate::Error> {
155        let raw = self.dump_raw(manifest_path)?;
156        parse_raw(&raw, manifest_path)
157    }
158}
159
160impl Default for RustDocBuilder {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166pub fn parse_raw(raw: &str, manifest_path: &std::path::Path) -> Result<crate::Api, crate::Error> {
167    RustDocParser::new().parse(raw, manifest_path)
168}
169
170#[derive(Default)]
171struct RustDocParser {
172    unprocessed: VecDeque<(Option<crate::PathId>, rustdoc_json_types_fork::Id)>,
173    deferred_imports: Vec<(crate::PathId, String, rustdoc_json_types_fork::Id)>,
174
175    api: crate::Api,
176    crate_ids: HashMap<u32, Option<crate::CrateId>>,
177    path_ids: HashMap<rustdoc_json_types_fork::Id, Option<crate::PathId>>,
178    item_ids: HashMap<rustdoc_json_types_fork::Id, Option<crate::ItemId>>,
179}
180
181impl RustDocParser {
182    fn new() -> Self {
183        Self::default()
184    }
185
186    fn parse(
187        mut self,
188        raw: &str,
189        manifest_path: &std::path::Path,
190    ) -> Result<crate::Api, crate::Error> {
191        let raw: rustdoc_json_types_fork::Crate = serde_json::from_str(raw).map_err(|e| {
192            crate::Error::new(
193                crate::ErrorKind::ApiParse,
194                format!(
195                    "Failed when parsing json for {}: {}",
196                    manifest_path.display(),
197                    e
198                ),
199            )
200        })?;
201
202        self.unprocessed.push_back((None, raw.root.clone()));
203        while let Some((parent_path_id, raw_item_id)) = self.unprocessed.pop_front() {
204            let raw_item = raw
205                .index
206                .get(&raw_item_id)
207                .expect("all item ids are in `index`");
208
209            let crate_id = self._parse_crate(&raw, raw_item.crate_id);
210
211            let path_id = self
212                ._parse_path(&raw, parent_path_id, &raw_item_id, crate_id)
213                .or(parent_path_id);
214
215            self._parse_item(&raw, &raw_item_id, path_id, crate_id);
216        }
217
218        for (parent_path_id, name, raw_target_id) in self.deferred_imports {
219            let target_path_id = self.path_ids.get(&raw_target_id).unwrap().unwrap();
220            let target_path = self
221                .api
222                .paths
223                .get(target_path_id)
224                .expect("path_id to always be valid")
225                .clone();
226
227            let parent_path = self
228                .api
229                .paths
230                .get(parent_path_id)
231                .expect("all ids are valid");
232            let name = format!("{}::{}", parent_path.path, name);
233
234            let kind = crate::PathKind::Import;
235
236            let mut path = crate::Path::new(kind, name);
237            path.crate_id = parent_path.crate_id;
238            path.item_id = target_path.item_id;
239            path.children = target_path.children.clone();
240            let path_id = self.api.paths.push(path);
241
242            self.api
243                .paths
244                .get_mut(parent_path_id)
245                .expect("parent_path_id to always be valid")
246                .children
247                .push(path_id);
248        }
249
250        Ok(self.api)
251    }
252
253    fn _parse_crate(
254        &mut self,
255        raw: &rustdoc_json_types_fork::Crate,
256        raw_crate_id: u32,
257    ) -> Option<crate::CrateId> {
258        if let Some(crate_id) = self.crate_ids.get(&raw_crate_id) {
259            return *crate_id;
260        }
261
262        let crate_id = (raw_crate_id != 0).then(|| {
263            let raw_crate = raw
264                .external_crates
265                .get(&raw_crate_id)
266                .expect("all crate ids are in `external_crates`");
267            let crate_ = crate::Crate::new(&raw_crate.name);
268            self.api.crates.push(crate_)
269        });
270        self.crate_ids.insert(raw_crate_id, crate_id);
271        crate_id
272    }
273
274    fn _parse_path(
275        &mut self,
276        raw: &rustdoc_json_types_fork::Crate,
277        parent_path_id: Option<crate::PathId>,
278        raw_item_id: &rustdoc_json_types_fork::Id,
279        crate_id: Option<crate::CrateId>,
280    ) -> Option<crate::PathId> {
281        if let Some(path_id) = self.path_ids.get(raw_item_id) {
282            return *path_id;
283        }
284
285        let path_id = raw.paths.get(raw_item_id).map(|raw_path| {
286            let raw_item = raw
287                .index
288                .get(raw_item_id)
289                .expect("all item ids are in `index`");
290
291            let kind = _convert_path_kind(raw_path.kind.clone());
292
293            let mut path = crate::Path::new(kind, raw_path.path.join("::"));
294            path.crate_id = crate_id;
295            path.span = raw_item.span.clone().map(|raw_span| crate::Span {
296                filename: raw_span.filename,
297                begin: raw_span.begin,
298                end: raw_span.end,
299            });
300            let path_id = self.api.paths.push(path);
301
302            if let Some(parent_path_id) = parent_path_id {
303                self.api
304                    .paths
305                    .get_mut(parent_path_id)
306                    .expect("parent_path_id to always be valid")
307                    .children
308                    .push(path_id);
309            }
310            self.api.root_id.get_or_insert(path_id);
311            path_id
312        });
313        self.path_ids.insert(raw_item_id.clone(), path_id);
314        path_id
315    }
316
317    fn _parse_item(
318        &mut self,
319        raw: &rustdoc_json_types_fork::Crate,
320        raw_item_id: &rustdoc_json_types_fork::Id,
321        path_id: Option<crate::PathId>,
322        crate_id: Option<crate::CrateId>,
323    ) -> Option<crate::ItemId> {
324        if let Some(item_id) = self.item_ids.get(raw_item_id) {
325            return *item_id;
326        }
327
328        let raw_item = raw
329            .index
330            .get(raw_item_id)
331            .expect("all item ids are in `index`");
332
333        let item_id = match &raw_item.inner {
334            rustdoc_json_types_fork::ItemEnum::Module(module) => {
335                self.unprocessed
336                    .extend(module.items.iter().map(move |i| (path_id, i.clone())));
337                None
338            }
339            rustdoc_json_types_fork::ItemEnum::Import(import) => {
340                let raw_target_id = import.id.as_ref().unwrap();
341                self.unprocessed.push_back((path_id, raw_target_id.clone()));
342                self.deferred_imports.push((
343                    path_id.unwrap(),
344                    import.name.clone(),
345                    raw_target_id.clone(),
346                ));
347                None
348            }
349            rustdoc_json_types_fork::ItemEnum::Trait(trait_) => {
350                self.unprocessed
351                    .extend(trait_.items.iter().map(move |i| (path_id, i.clone())));
352                None
353            }
354            rustdoc_json_types_fork::ItemEnum::Impl(impl_) => {
355                self.unprocessed
356                    .extend(impl_.items.iter().map(move |i| (path_id, i.clone())));
357                None
358            }
359            rustdoc_json_types_fork::ItemEnum::Enum(enum_) => {
360                self.unprocessed
361                    .extend(enum_.variants.iter().map(move |i| (path_id, i.clone())));
362                None
363            }
364            _ => {
365                assert_ne!(self.api.root_id, None, "Module should be root");
366                let mut item = crate::Item::new();
367                item.crate_id = crate_id;
368                item.name = raw_item.name.clone();
369                item.span = raw_item.span.clone().map(|raw_span| crate::Span {
370                    filename: raw_span.filename,
371                    begin: raw_span.begin,
372                    end: raw_span.end,
373                });
374                let item_id = self.api.items.push(item);
375
376                if let Some(path_id) = path_id {
377                    self.api
378                        .paths
379                        .get_mut(path_id)
380                        .expect("path_id to always be valid")
381                        .item_id = Some(item_id);
382                }
383                Some(item_id)
384            }
385        };
386        self.item_ids.insert(raw_item_id.clone(), item_id);
387        item_id
388    }
389}
390
391fn _convert_path_kind(kind: rustdoc_json_types_fork::ItemKind) -> crate::PathKind {
392    match kind {
393        rustdoc_json_types_fork::ItemKind::Module => crate::PathKind::Module,
394        rustdoc_json_types_fork::ItemKind::ExternCrate => crate::PathKind::ExternCrate,
395        rustdoc_json_types_fork::ItemKind::Import => crate::PathKind::Import,
396        rustdoc_json_types_fork::ItemKind::Struct => crate::PathKind::Struct,
397        rustdoc_json_types_fork::ItemKind::Union => crate::PathKind::Union,
398        rustdoc_json_types_fork::ItemKind::Enum => crate::PathKind::Enum,
399        rustdoc_json_types_fork::ItemKind::Variant => crate::PathKind::Variant,
400        rustdoc_json_types_fork::ItemKind::Function => crate::PathKind::Function,
401        rustdoc_json_types_fork::ItemKind::Typedef => crate::PathKind::Typedef,
402        rustdoc_json_types_fork::ItemKind::OpaqueTy => crate::PathKind::OpaqueTy,
403        rustdoc_json_types_fork::ItemKind::Constant => crate::PathKind::Constant,
404        rustdoc_json_types_fork::ItemKind::Trait => crate::PathKind::Trait,
405        rustdoc_json_types_fork::ItemKind::TraitAlias => crate::PathKind::TraitAlias,
406        rustdoc_json_types_fork::ItemKind::Method => crate::PathKind::Method,
407        rustdoc_json_types_fork::ItemKind::Impl => crate::PathKind::Impl,
408        rustdoc_json_types_fork::ItemKind::Static => crate::PathKind::Static,
409        rustdoc_json_types_fork::ItemKind::ForeignType => crate::PathKind::ForeignType,
410        rustdoc_json_types_fork::ItemKind::Macro => crate::PathKind::Macro,
411        rustdoc_json_types_fork::ItemKind::ProcAttribute => crate::PathKind::ProcAttribute,
412        rustdoc_json_types_fork::ItemKind::ProcDerive => crate::PathKind::ProcDerive,
413        rustdoc_json_types_fork::ItemKind::AssocConst => crate::PathKind::AssocConst,
414        rustdoc_json_types_fork::ItemKind::AssocType => crate::PathKind::AssocType,
415        rustdoc_json_types_fork::ItemKind::Primitive => crate::PathKind::Primitive,
416        rustdoc_json_types_fork::ItemKind::Keyword => crate::PathKind::Keyword,
417        rustdoc_json_types_fork::ItemKind::StructField => {
418            unreachable!("These are handled by the Item")
419        }
420    }
421}