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 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 pub fn silence(mut self, yes: bool) -> Self {
45 self.silence = yes;
46 self
47 }
48
49 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 .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 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()) .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}