diff --git a/Cargo.lock b/Cargo.lock index 34fc0860a4b75..d2c299cb1dfb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2177,6 +2177,7 @@ dependencies = [ name = "linkchecker" version = "0.1.0" dependencies = [ + "clap", "html5ever", "regex", ] @@ -3184,6 +3185,17 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relnotes-api-list" +version = "0.1.0" +dependencies = [ + "anyhow", + "rustdoc-json-types", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "remote-test-client" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6d3425f4115a1..97e38f41468ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "src/tools/miri/cargo-miri", "src/tools/miropt-test-tools", "src/tools/opt-dist", + "src/tools/relnotes-api-list", "src/tools/remote-test-client", "src/tools/remote-test-server", "src/tools/replace-version-placeholder", diff --git a/src/bootstrap/src/core/build_steps/dist.rs b/src/bootstrap/src/core/build_steps/dist.rs index 25b7e5a1b5d1c..b3698fb98f88f 100644 --- a/src/bootstrap/src/core/build_steps/dist.rs +++ b/src/bootstrap/src/core/build_steps/dist.rs @@ -2636,3 +2636,62 @@ impl Step for Gcc { tarball.generate() } } + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct RelnotesApiList { + pub host: TargetSelection, +} + +impl Step for RelnotesApiList { + type Output = (); + const DEFAULT: bool = true; + + fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { + let default = run.builder.config.docs; + run.alias("relnotes-api-list").default_condition(default) + } + + fn make_run(run: RunConfig<'_>) { + run.builder.ensure(RelnotesApiList { host: run.target }); + } + + fn run(self, builder: &Builder<'_>) -> Self::Output { + let host = self.host; + let dest = builder.out.join("dist").join(format!("relnotes-api-list-{host}.json")); + builder.create_dir(dest.parent().unwrap()); + + // The HTML documentation for the standard library is needed to check all links generated by + // the tool are not broken. + builder.ensure(crate::core::build_steps::doc::Std::new( + builder.top_stage, + host, + DocumentationFormat::Html, + )); + + if std::env::var_os("EMILY_SKIP_DOC").is_none() { + // TODO: remove the condition + builder.ensure( + crate::core::build_steps::doc::Std::new( + builder.top_stage, + host, + DocumentationFormat::Json, + ) + // Crates containing symbols exported by any std crate: + .add_extra_crate("rustc-literal-escaper") + .add_extra_crate("std_detect"), + ); + } + + let linkchecker = builder.tool_exe(Tool::Linkchecker); + + builder.info("Generating the API list for the release notes"); + builder + .tool_cmd(Tool::RelnotesApiList) + .arg(builder.json_doc_out(host)) + .arg(&dest) + .env("LINKCHECKER_PATH", linkchecker) + .env("STD_HTML_DOCS", builder.doc_out(self.host)) + .run(builder); + builder.info(&format!("API list for the release notes available at {}", dest.display())); + } +} diff --git a/src/bootstrap/src/core/build_steps/doc.rs b/src/bootstrap/src/core/build_steps/doc.rs index f7c4c5ad0bbd3..6a8862a4fd5e9 100644 --- a/src/bootstrap/src/core/build_steps/doc.rs +++ b/src/bootstrap/src/core/build_steps/doc.rs @@ -560,11 +560,17 @@ pub struct Std { pub target: TargetSelection, pub format: DocumentationFormat, crates: Vec, + extra_crates: Vec, } impl Std { pub(crate) fn new(stage: u32, target: TargetSelection, format: DocumentationFormat) -> Self { - Std { stage, target, format, crates: vec![] } + Std { stage, target, format, crates: vec![], extra_crates: vec![] } + } + + pub(crate) fn add_extra_crate(mut self, krate: &str) -> Self { + self.extra_crates.push(krate.to_string()); + self } } @@ -592,6 +598,7 @@ impl Step for Std { DocumentationFormat::Html }, crates, + extra_crates: vec![], }); } @@ -602,7 +609,7 @@ impl Step for Std { fn run(self, builder: &Builder<'_>) { let stage = self.stage; let target = self.target; - let crates = if self.crates.is_empty() { + let mut crates = if self.crates.is_empty() { builder .in_tree_crates("sysroot", Some(target)) .iter() @@ -611,6 +618,7 @@ impl Step for Std { } else { self.crates }; + crates.extend(self.extra_crates.iter().cloned()); let out = match self.format { DocumentationFormat::Html => builder.doc_out(target), diff --git a/src/bootstrap/src/core/build_steps/tool.rs b/src/bootstrap/src/core/build_steps/tool.rs index ad3f8d8976701..a47073137f9aa 100644 --- a/src/bootstrap/src/core/build_steps/tool.rs +++ b/src/bootstrap/src/core/build_steps/tool.rs @@ -526,6 +526,7 @@ bootstrap_tool!( FeaturesStatusDump, "src/tools/features-status-dump", "features-status-dump"; OptimizedDist, "src/tools/opt-dist", "opt-dist", submodules = &["src/tools/rustc-perf"]; RunMakeSupport, "src/tools/run-make-support", "run_make_support", artifact_kind = ToolArtifactKind::Library; + RelnotesApiList, "src/tools/relnotes-api-list", "relnotes-api-list"; ); /// These are the submodules that are required for rustbook to work due to diff --git a/src/bootstrap/src/core/builder/mod.rs b/src/bootstrap/src/core/builder/mod.rs index b96a988cde3ff..5a8ef5c42e3c4 100644 --- a/src/bootstrap/src/core/builder/mod.rs +++ b/src/bootstrap/src/core/builder/mod.rs @@ -980,6 +980,7 @@ impl<'a> Builder<'a> { tool::CoverageDump, tool::LlvmBitcodeLinker, tool::RustcPerf, + tool::RelnotesApiList, ), Kind::Clippy => describe!( clippy::Std, @@ -1156,7 +1157,8 @@ impl<'a> Builder<'a> { dist::PlainSourceTarball, dist::BuildManifest, dist::ReproducibleArtifacts, - dist::Gcc + dist::Gcc, + dist::RelnotesApiList, ), Kind::Install => describe!( install::Docs, diff --git a/src/tools/linkchecker/Cargo.toml b/src/tools/linkchecker/Cargo.toml index 7123d43eb564c..c851027966542 100644 --- a/src/tools/linkchecker/Cargo.toml +++ b/src/tools/linkchecker/Cargo.toml @@ -10,3 +10,4 @@ path = "main.rs" [dependencies] regex = "1" html5ever = "0.29.0" +clap = { version = "4.5.40", features = ["derive"] } diff --git a/src/tools/linkchecker/main.rs b/src/tools/linkchecker/main.rs index 84cba3f8c4473..8bc5d43b3f844 100644 --- a/src/tools/linkchecker/main.rs +++ b/src/tools/linkchecker/main.rs @@ -18,16 +18,19 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; +use std::fs; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; use std::rc::Rc; use std::time::Instant; -use std::{env, fs}; +use clap::Parser; use html5ever::tendril::ByteTendril; use html5ever::tokenizer::{ BufferQueue, TagToken, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts, }; +use std::collections::hash_map::Entry; +use std::iter::once; // Add linkcheck exceptions here // If at all possible you should use intra-doc links to avoid linkcheck issues. These @@ -110,10 +113,19 @@ macro_rules! t { }; } +#[derive(Parser)] +struct Cli { + docs: PathBuf, + #[clap(long)] + extra_target: Vec, +} + fn main() { - let docs = env::args_os().nth(1).expect("doc path should be first argument"); - let docs = env::current_dir().unwrap().join(docs); - let mut checker = Checker { root: docs.clone(), cache: HashMap::new() }; + let mut cli = Cli::parse(); + cli.docs = cli.docs.canonicalize().unwrap(); + + let mut checker = + Checker { root: cli.docs.clone(), extra_targets: cli.extra_target, cache: HashMap::new() }; let mut report = Report { errors: 0, start: Instant::now(), @@ -125,7 +137,7 @@ fn main() { intra_doc_exceptions: 0, has_broken_urls: false, }; - checker.walk(&docs, &mut report); + checker.walk(&cli.docs, &mut report); report.report(); if report.errors != 0 { println!("found some broken links"); @@ -135,6 +147,7 @@ fn main() { struct Checker { root: PathBuf, + extra_targets: Vec, cache: Cache, } @@ -427,15 +440,24 @@ impl Checker { let pretty_path = file.strip_prefix(&self.root).unwrap_or(file).to_str().unwrap().to_string(); - let entry = - self.cache.entry(pretty_path.clone()).or_insert_with(|| match fs::metadata(file) { + for base in once(&self.root).chain(self.extra_targets.iter()) { + // TODO: rebase and turn into a let else + let entry = self.cache.entry(pretty_path.clone()); + if let Entry::Occupied(e) = &entry { + if !matches!(e.get(), FileEntry::Missing) { + break; + } + } + + let file = base.join(&pretty_path); + entry.insert_entry(match fs::metadata(&file) { Ok(metadata) if metadata.is_dir() => FileEntry::Dir, Ok(_) => { if file.extension().and_then(|s| s.to_str()) != Some("html") { FileEntry::OtherFile } else { report.html_files += 1; - load_html_file(file, report) + load_html_file(&file, report) } } Err(e) if e.kind() == ErrorKind::NotFound => FileEntry::Missing, @@ -451,6 +473,9 @@ impl Checker { panic!("unexpected read error for {}: {}", file.display(), e); } }); + } + + let entry = self.cache.get(&pretty_path).unwrap(); (pretty_path, entry) } } diff --git a/src/tools/relnotes-api-list/Cargo.toml b/src/tools/relnotes-api-list/Cargo.toml new file mode 100644 index 0000000000000..72e416ee50b67 --- /dev/null +++ b/src/tools/relnotes-api-list/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "relnotes-api-list" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rustdoc-json-types = { path = "../../rustdoc-json-types" } +tempfile = "3.20.0" diff --git a/src/tools/relnotes-api-list/README.md b/src/tools/relnotes-api-list/README.md new file mode 100644 index 0000000000000..e19f0bf4ca50c --- /dev/null +++ b/src/tools/relnotes-api-list/README.md @@ -0,0 +1,17 @@ +# API list generator for the release notes + +Rust's [release notes] include a "Stabilized APIs" section for each +release, listing all APIs that became stable each release. This tool supports +the creation of that section by generating a JSON file containing a concise +representation of the standard library API. The [relnotes tool] will then +compare the JSON files of two releases to generate the section. + +The tool is executed by CI and produces the `relnotes-api-list-$target.json` +dist artifact. You can also run the tool locally with: + +``` +./x dist relnotes-api-list +``` + +[release notes]: https://doc.rust-lang.org/stable/releases.html +[relnotes tool]: https://github.com/rust-lang/relnotes diff --git a/src/tools/relnotes-api-list/src/check_urls.rs b/src/tools/relnotes-api-list/src/check_urls.rs new file mode 100644 index 0000000000000..ad25988222e8a --- /dev/null +++ b/src/tools/relnotes-api-list/src/check_urls.rs @@ -0,0 +1,63 @@ +//! We generate a lot of URLs in the resulting JSON, and we need all those URLs to be correct. While +//! some URLs are fairly trivial to generate, others are quite tricky (especially `impl` blocks). +//! +//! To ensure we always generate good URLs, we prepare a temporary HTML file containing `` tags for +//! every itme we collected, and we run it through linkchecker. If this fails, it means the code +//! generating URLs has a bug. + +use crate::schema::{Schema, SchemaItem}; +use anyhow::{Error, anyhow, bail}; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::process::Command; +use tempfile::tempdir; + +pub(crate) fn check_urls(schema: &Schema) -> Result<(), Error> { + let mut urls = Vec::new(); + collect_urls(&mut urls, &schema.items); + + let html_dir = tempdir()?; + + let mut file = File::create(html_dir.path().join("urls.html"))?; + file.write_all(render_html(&urls).as_bytes())?; + + eprintln!("checking that all generated URLs are valid..."); + let result = Command::new(require_env("LINKCHECKER_PATH")?) + .arg(html_dir.path()) + .arg("--extra-target") + .arg(require_env("STD_HTML_DOCS")?) + .status()?; + + if !result.success() { + bail!("some URLs are broken, the relnotes-api-list tool has a bug"); + } + + dbg!(require_env("STD_HTML_DOCS")?); + + Ok(()) +} + +fn collect_urls<'a>(result: &mut Vec<&'a str>, items: &'a [SchemaItem]) { + for item in items { + if let Some(url) = &item.url { + result.push(url); + } + collect_urls(result, &item.children); + } +} + +fn render_html(urls: &[&str]) -> String { + let mut content = "\n".to_string(); + for url in urls { + content.push_str(&format!("\n")); + } + content +} + +fn require_env(name: &str) -> Result { + match std::env::var_os(name) { + Some(value) => Ok(value.into()), + None => Err(anyhow!("missing environment variable {name}")), + } +} diff --git a/src/tools/relnotes-api-list/src/convert_to_schema.rs b/src/tools/relnotes-api-list/src/convert_to_schema.rs new file mode 100644 index 0000000000000..c7747a251c884 --- /dev/null +++ b/src/tools/relnotes-api-list/src/convert_to_schema.rs @@ -0,0 +1,328 @@ +use crate::PUBLIC_CRATES; +use crate::pretty_print::pretty_impl; +use crate::schema::SchemaItem; +use crate::stability::{Stability, StabilityStore}; +use crate::store::{Store, StoreCrateId, StoreItem}; +use crate::visitor::{Visitor, walk_item}; +use anyhow::{Context, Error, bail}; +use rustdoc_json_types::{Id, ItemEnum, MacroKind, Visibility}; +use std::collections::HashSet; + +pub(crate) struct ConvertToSchema<'a> { + store: &'a Store, + stability: &'a StabilityStore, + + within_impl: bool, + within_trait: bool, + url_stack: Vec, + name_stack: Vec, + reachable_without_use: HashSet<(StoreCrateId, Id)>, +} + +impl<'a> Visitor<'a> for ConvertToSchema<'a> { + type Result = Vec; + + fn visit_item(&mut self, item: &StoreItem<'a>) -> Result, Error> { + if item.visibility != self.expected_visibility(item) { + return Ok(Vec::new()); + } + + match (self.stability.get(item.crate_id, item.id), &item.inner) { + (Some(Stability::Stable), _) => {} + (Some(Stability::Unstable), _) => return Ok(Vec::new()), + + // `impl` blocks are not required to have stability attributes, so we should not error + // out if they are missing them. + (None, ItemEnum::Impl(_)) => {} + + (None, _) => bail!( + "stability attribute is missing for {} {:?}", + self.name_stack.join("::"), + item.id + ), + } + + let mut pop_name = false; + if let Some(name) = &item.name { + self.name_stack.push(name.clone()); + pop_name = true; + } + + let mut pop_url = false; + match self.url_fragment(item)? { + UrlChunk::Directory(dir) => { + pop_url = true; + self.url_stack.push(format!("{dir}/")); + } + UrlChunk::Item(kind) => { + pop_url = true; + let name = item.require_name()?; + if self.url_stack.last().map(|last| last.ends_with(".html")).unwrap_or(false) { + self.url_stack.push(format!("#{kind}.{name}")) + } else { + self.url_stack.push(format!("{kind}.{name}.html")); + } + } + UrlChunk::None => {} + } + + if self.url_stack.join("") == "std/io/prelude/trait.BufRead.html" { + //bail!("foo"); + } + + let mut restore_within_impl = None; + let mut restore_within_trait = None; + let result = match &item.inner { + ItemEnum::AssocConst { .. } + | ItemEnum::AssocType { .. } + | ItemEnum::Constant { .. } + | ItemEnum::Enum(_) + | ItemEnum::Macro(_) + | ItemEnum::Module(_) + | ItemEnum::Static(_) + | ItemEnum::Struct(_) + | ItemEnum::StructField(_) + | ItemEnum::TypeAlias(_) + | ItemEnum::Union(_) + | ItemEnum::Variant(_) + | ItemEnum::ProcMacro(_) + | ItemEnum::Function(_) => self.include(item), + + ItemEnum::Trait(_) => { + restore_within_trait = Some(self.within_trait); + self.within_trait = true; + + self.include(item) + } + + ItemEnum::Primitive(p) => { + // We don't want primitives to have the `std::` prefix. + let old_stack = std::mem::replace(&mut self.name_stack, vec![p.name.clone()]); + let result = self.include(item); + self.name_stack = old_stack; + result + } + + ItemEnum::Use(_) => { + if self.should_inline_use(item)? { + walk_item(self, item) + } else { + // TODO: do we want to include an item for public re-exports? Probably yes! + Ok(Vec::new()) + } + } + + ItemEnum::Impl(impl_) => { + if impl_.trait_.is_some() { + Ok(vec![SchemaItem { + name: pretty_impl(impl_), + deprecated: item.deprecation.is_some(), + url: None, // TODO: is there an URL we can put here? + + // We are intentionally not walking inside impls of traits: we don't want + // all types in the standard library to show up in the changelog if a new + // item is added in a trait. + children: Vec::new(), + }]) + } else { + restore_within_impl = Some(self.within_impl); + self.within_impl = true; + + walk_item(self, item) + } + } + + ItemEnum::TraitAlias(_) | ItemEnum::ExternType | ItemEnum::ExternCrate { .. } => { + Ok(Vec::new()) + } + }; + + if pop_name { + self.name_stack.pop(); + } + if pop_url { + self.url_stack.pop(); + } + if let Some(restore) = restore_within_impl { + self.within_impl = restore; + } + if let Some(restore) = restore_within_trait { + self.within_trait = restore; + } + result + } + + fn store(&self) -> &'a Store { + self.store + } +} + +impl<'a> ConvertToSchema<'a> { + pub(crate) fn new(store: &'a Store, stability: &'a StabilityStore) -> Result { + Ok(Self { + store, + stability, + within_impl: false, + within_trait: false, + name_stack: Vec::new(), + url_stack: Vec::new(), + reachable_without_use: reachable_without_use(store)?, + }) + } + + fn include(&mut self, item: &StoreItem<'a>) -> Result, Error> { + let item = SchemaItem { + name: self.name_stack.join("::"), + url: Some(self.current_url()), + deprecated: item.deprecation.is_some(), + children: walk_item(self, item)?, + }; + Ok(vec![item]) + } + + fn should_inline_use(&self, use_: &StoreItem<'a>) -> Result { + for attr in &use_.attrs { + if attr == "#[doc(no_inline)]" { + return Ok(false); + } else if attr == "#[doc(inline)]" { + return Ok(true); + } + } + + let resolved = self.store.resolve_use_recursive(use_.crate_id, use_.id)?; + Ok(!self.reachable_without_use.contains(&(resolved.krate, resolved.item))) + } + + fn current_url(&self) -> String { + let mut url = self.url_stack.join(""); + if url.ends_with('/') { + url.push_str("index.html"); + } + url + } + + fn url_fragment(&self, item: &StoreItem<'a>) -> Result, Error> { + Ok(UrlChunk::Item(match &item.inner { + // The fragment returned here will be used here as `{fragment}.{name}.html` or + // `#{fragment}.{name}`, depending on whether the item is nested in another item. + ItemEnum::Union(_) => "union", + ItemEnum::Struct(_) => "struct", + ItemEnum::StructField(_) => "structfield", + ItemEnum::Enum(_) => "enum", + ItemEnum::Variant(_) => "variant", + ItemEnum::Function(f) if self.within_trait && !f.has_body => "tymethod", + ItemEnum::Function(_) if self.within_impl || self.within_trait => "method", + ItemEnum::Function(_) => "fn", + ItemEnum::Trait(_) => "trait", + ItemEnum::Constant { .. } => "constant", + ItemEnum::Static(_) => "static", + ItemEnum::Macro(_) => "macro", + ItemEnum::Primitive(_) => "primitive", + ItemEnum::AssocConst { .. } => "associatedconstant", + ItemEnum::AssocType { .. } => "associatedtype", + ItemEnum::ProcMacro(proc_macro) => match proc_macro.kind { + MacroKind::Bang => "macro", + MacroKind::Attr => "attr", + MacroKind::Derive => "derive", + }, + + ItemEnum::Module(_) => return Ok(UrlChunk::Directory(item.require_name()?)), + + ItemEnum::ExternType + | ItemEnum::ExternCrate { .. } + | ItemEnum::Impl(_) + | ItemEnum::TraitAlias(_) + | ItemEnum::TypeAlias(_) + | ItemEnum::Use(_) => return Ok(UrlChunk::None), + })) + } + + /// Some items don't have a visibility associated to them, and are instead public by default. This + /// function determines what visibility a public item must have. + fn expected_visibility(&self, item: &StoreItem<'_>) -> Visibility { + if self.within_trait { + return Visibility::Default; + } + + match &item.inner { + ItemEnum::Variant(_) | ItemEnum::Impl(_) => Visibility::Default, + + ItemEnum::AssocType { .. } + | ItemEnum::AssocConst { .. } + | ItemEnum::Module(_) + | ItemEnum::ExternCrate { .. } + | ItemEnum::Use(_) + | ItemEnum::Union(_) + | ItemEnum::Struct(_) + | ItemEnum::StructField(_) + | ItemEnum::Enum(_) + | ItemEnum::Function(_) + | ItemEnum::Trait(_) + | ItemEnum::TraitAlias(_) + | ItemEnum::TypeAlias(_) + | ItemEnum::Constant { .. } + | ItemEnum::Static(_) + | ItemEnum::ExternType + | ItemEnum::Macro(_) + | ItemEnum::ProcMacro(_) + | ItemEnum::Primitive(_) => Visibility::Public, + } + } +} + +enum UrlChunk<'a> { + Directory(&'a str), + Item(&'static str), + None, +} + +/// Unless overridden with `#[doc(inline)]` or `#[doc(no_inline)]`, rustdoc chooses whether to +/// inline an `use` or not by checking if it's otherwise reachable without `use`s. For example: +/// +/// * `core::prelude::v1::Iterator` should not be inlined, since it's an `use` pointing to +/// `core::iter::Iterator`, which is accessible directly. +/// +/// * `core::mem::MaybeUninit` should be inlined, since it's an `use` pointing to the *private* +/// `core::mem::maybe_uninit::MaybeUninit`. +/// +/// This function walks the JSON to collect all items that are reachable without an use. +fn reachable_without_use(store: &Store) -> Result, Error> { + struct ReachableWithoutUse<'a> { + store: &'a Store, + reachable: HashSet<(StoreCrateId, Id)>, + } + + impl<'a> Visitor<'a> for ReachableWithoutUse<'a> { + type Result = (); + + fn store(&self) -> &'a Store { + self.store + } + + fn visit_item(&mut self, item: &StoreItem<'a>) -> Result<(), Error> { + // Don't walk into `use`s. + if let ItemEnum::Use(_) = &item.inner { + return Ok(()); + } + + self.reachable.insert((item.crate_id, item.id)); + walk_item(self, item) + } + } + + let mut visitor = ReachableWithoutUse { store, reachable: HashSet::new() }; + for crate_id in store.crate_ids() { + // Items defined in non-public dependencies (for example, as of 2025, the std_detect crate), + // are never reachable in the documentation without a `use`, since we don't generate docs + // for those crates. So, only look in publicly accessible crates. + if PUBLIC_CRATES.contains(&store.crate_name(crate_id)) { + visitor.visit_item(&store.crate_root(crate_id)?).with_context(|| { + format!( + "failed to check the reachability of items in crate {}", + store.crate_name(crate_id) + ) + })?; + } + } + Ok(visitor.reachable) +} diff --git a/src/tools/relnotes-api-list/src/main.rs b/src/tools/relnotes-api-list/src/main.rs new file mode 100644 index 0000000000000..872c7d4252279 --- /dev/null +++ b/src/tools/relnotes-api-list/src/main.rs @@ -0,0 +1,73 @@ +use crate::check_urls::check_urls; +use crate::convert_to_schema::ConvertToSchema; +use crate::schema::{CURRENT_SCHEMA_VERSION, Schema, SchemaItem}; +use crate::stability::StabilityStore; +use crate::store::Store; +use crate::visitor::Visitor; +use anyhow::{Context as _, Error}; + +mod check_urls; +mod convert_to_schema; +mod pretty_print; +mod schema; +mod stability; +mod store; +mod visitor; + +/// List of crates that will be included in the API list. Note that this also affects URL +/// generation: removing crates we generate documentation for, or adding crates we don't, will +/// result in broken URLs. +static PUBLIC_CRATES: &[&str] = &["core", "alloc", "std", "test", "proc_macro"]; + +fn main() -> Result<(), Error> { + let args = std::env::args_os().skip(1).collect::>(); + let [rustdoc_json_dir, dest] = args.as_slice() else { + eprintln!("usage: relnotes-api-list "); + std::process::exit(1) + }; + + let mut store = Store::new(); + let mut crates_to_emit = Vec::new(); + for entry in std::fs::read_dir(rustdoc_json_dir)? { + let path = entry?.path(); + if path.extension().and_then(|s| s.to_str()) != Some("json") { + continue; + } + let id = store.load(&path).with_context(|| format!("failed to parse {path:?}"))?; + if PUBLIC_CRATES.contains(&store.crate_name(id)) { + crates_to_emit.push(id); + } + } + + let mut stability = StabilityStore::new(); + for id in store.crate_ids() { + stability + .add(&store, &store.crate_root(id)?) + .with_context(|| format!("failed to gather stability for {}", store.crate_name(id)))?; + } + + let mut result = Schema { schema_version: CURRENT_SCHEMA_VERSION, items: Vec::new() }; + for id in crates_to_emit { + result.items.extend( + ConvertToSchema::new(&store, &stability)? + .visit_item(&store.crate_root(id)?) + .with_context(|| format!("failed to process crate {}", store.crate_name(id)))?, + ); + } + std::fs::write(dest, &serde_json::to_string_pretty(&result)?)?; + + check_urls(&result)?; + + eprintln!("found {} documentation items", count_items(&result.items)); + + Ok(()) +} + +fn count_items(items: &[SchemaItem]) -> usize { + let mut count = 0; + for item in items { + count += 1; + count += count_items(&item.children); + } + count +} diff --git a/src/tools/relnotes-api-list/src/pretty_print.rs b/src/tools/relnotes-api-list/src/pretty_print.rs new file mode 100644 index 0000000000000..60f8993a412fe --- /dev/null +++ b/src/tools/relnotes-api-list/src/pretty_print.rs @@ -0,0 +1,383 @@ +//! Convert the JSON representation of an `impl` signature into a textual representation of it, +//! closely aligning to Rust syntax. +//! +//! **WARNING:** changing how existing `impl` blocks are rendered will result in them showing up as +//! new items in the release notes. You should avoid making changes to this file, other than adding +//! support for new JSON types. + +use std::convert::identity; + +use rustdoc_json_types::{ + Abi, AssocItemConstraintKind, Constant, FunctionHeader, FunctionSignature, GenericArg, + GenericArgs, GenericBound, GenericParamDef, GenericParamDefKind, Impl, Path, PolyTrait, + PreciseCapturingArg, Term, TraitBoundModifier, Type, WherePredicate, +}; + +pub(crate) fn pretty_impl(impl_: &Impl) -> String { + let mut result = "impl".to_string(); + + if !impl_.generics.params.is_empty() { + result.push('<'); + result.push_str(&comma_separated(&impl_.generics.params, pretty_generic_param_def)); + result.push('>'); + } + + result.push(' '); + if let Some(trait_) = &impl_.trait_ { + if impl_.is_negative { + result.push('!'); + } + result.push_str(&pretty_path(&trait_)); + result.push_str(" for "); + } + result.push_str(&pretty_type(&impl_.for_)); + + if !impl_.generics.where_predicates.is_empty() { + result.push_str(" where "); + result + .push_str(&comma_separated(&impl_.generics.where_predicates, pretty_where_predicates)); + } + + result +} + +pub(crate) fn pretty_path(path: &Path) -> String { + let mut result = path.path.clone(); + if let Some(generic_args) = &path.args { + result.push_str(&pretty_generic_args(generic_args)); + } + result +} + +fn pretty_generic_args(generic_args: &GenericArgs) -> String { + match generic_args { + GenericArgs::AngleBracketed { args, constraints } => { + let mut result = "<".to_string(); + for arg in args { + if result.len() > 1 { + result.push_str(", "); + } + result.push_str(&pretty_generic_arg(arg)); + } + for constraint in constraints { + if result.len() > 1 { + result.push_str(", "); + } + result.push_str(&constraint.name); + if let Some(args) = &constraint.args { + result.push_str(&pretty_generic_args(args)); + } + match &constraint.binding { + AssocItemConstraintKind::Equality(term) => { + result.push_str(" = "); + result.push_str(&pretty_term(term)); + } + AssocItemConstraintKind::Constraint(bounds) => { + if !bounds.is_empty() { + result.push_str(": "); + result.push_str(&plus_separated(bounds, pretty_generic_bound)); + } + } + } + } + result.push('>'); + result + } + GenericArgs::Parenthesized { inputs, output } => { + let mut result = format!("({})", comma_separated(inputs.iter(), pretty_type)); + if let Some(return_) = output { + result.push_str(" -> "); + result.push_str(&pretty_type(return_)); + } + result + } + GenericArgs::ReturnTypeNotation => "(..)".to_string(), + } +} + +fn pretty_generic_arg(generic_arg: &GenericArg) -> String { + match generic_arg { + GenericArg::Lifetime(lifetime) => lifetime.clone(), + GenericArg::Type(ty) => pretty_type(ty), + GenericArg::Const(const_) => pretty_constant(const_), + GenericArg::Infer => "_".into(), + } +} + +fn pretty_constant(constant: &Constant) -> String { + if let Some(value) = &constant.value { value.clone() } else { constant.expr.clone() } +} + +fn pretty_term(term: &Term) -> String { + match term { + Term::Type(type_) => pretty_type(&type_), + Term::Constant(constant) => pretty_constant(&constant), + } +} + +fn pretty_type(ty: &Type) -> String { + match ty { + Type::ResolvedPath(path) => pretty_path(path), + Type::DynTrait(dyn_trait) => { + let mut result = + format!("dyn {}", plus_separated(dyn_trait.traits.iter(), pretty_poly_trait)); + if let Some(lifetime) = &dyn_trait.lifetime { + result.push_str(lifetime); + } + result + } + Type::Generic(generic) => generic.clone(), + Type::Primitive(primitive) => primitive.clone(), + Type::FunctionPointer(function) => { + format!( + "{}{}fn{}", + pretty_function_header(&function.header), + pretty_hrtb(&function.generic_params), + pretty_function_signature(&function.sig), + ) + } + Type::Tuple(inner) => format!("({})", comma_separated(inner, pretty_type)), + Type::Slice(inner) => format!("[{}]", pretty_type(inner)), + Type::Array { type_, len } => format!("[{}; {len}]", pretty_type(type_)), + Type::Pat { .. } => panic!("pattern type visible in the public api"), + Type::ImplTrait(bounds) => { + format!("impl {}", plus_separated(bounds, pretty_generic_bound)) + } + Type::Infer => "_".into(), + Type::RawPointer { is_mutable, type_ } => { + format!("*{} {}", if *is_mutable { "mut" } else { "const" }, pretty_type(type_)) + } + Type::BorrowedRef { lifetime, is_mutable, type_ } => match (lifetime, is_mutable) { + (Some(lifetime), false) => format!("&{lifetime} {}", pretty_type(type_)), + (Some(lifetime), true) => format!("&{lifetime} mut {}", pretty_type(type_)), + (None, false) => format!("&{}", pretty_type(type_)), + (None, true) => format!("&mut {}", pretty_type(type_)), + }, + Type::QualifiedPath { name, args, self_type, trait_ } => { + let mut result = format!("<{}", pretty_type(self_type)); + if let Some(trait_) = trait_ { + result.push_str(" as "); + result.push_str(&pretty_path(&trait_)); + } + result.push_str(">::"); + result.push_str(name); + if let Some(args) = args { + result.push_str(&pretty_generic_args(args)); + } + result + } + } +} + +fn pretty_poly_trait(poly: &PolyTrait) -> String { + format!("{}{}", pretty_hrtb(&poly.generic_params), pretty_path(&poly.trait_)) +} + +fn pretty_hrtb(gpds: &[GenericParamDef]) -> String { + let mut result = String::new(); + if !gpds.is_empty() { + result.push_str("for<"); + comma_separated(gpds, pretty_generic_param_def); + result.push_str("> "); + } + result +} + +fn pretty_generic_param_def(gpd: &GenericParamDef) -> Option { + match &gpd.kind { + GenericParamDefKind::Lifetime { outlives } => { + if !outlives.is_empty() { + Some(format!("{}: {}", gpd.name, plus_separated(outlives, identity))) + } else { + Some(gpd.name.clone()) + } + } + GenericParamDefKind::Type { bounds, default, is_synthetic } => { + if *is_synthetic { + return None; + } + let mut result = gpd.name.clone(); + if !bounds.is_empty() { + result.push_str(": "); + result.push_str(&plus_separated(bounds, pretty_generic_bound)); + } + if let Some(default) = default { + result.push_str(" = "); + result.push_str(&pretty_type(default)); + } + Some(result) + } + GenericParamDefKind::Const { type_, default } => { + let mut result = format!("const {}: {}", gpd.name, pretty_type(type_)); + if let Some(default) = default { + result.push_str(" = "); + result.push_str(default); + } + Some(result) + } + } +} + +fn pretty_generic_bound(bound: &GenericBound) -> String { + match bound { + GenericBound::TraitBound { trait_, generic_params, modifier } => { + let mut result = format!("{}{}", pretty_hrtb(generic_params), pretty_path(trait_)); + match modifier { + TraitBoundModifier::None => {} + TraitBoundModifier::Maybe => result.push('?'), + TraitBoundModifier::MaybeConst => {} + } + result + } + GenericBound::Outlives(lifetime) => format!("{lifetime}"), + GenericBound::Use(use_) => { + format!( + "use<{}>", + comma_separated(use_, |arg| match arg { + PreciseCapturingArg::Lifetime(lifetime) => lifetime, + PreciseCapturingArg::Param(param) => param, + }) + ) + } + } +} + +fn pretty_where_predicates(predicate: &WherePredicate) -> String { + match predicate { + WherePredicate::BoundPredicate { type_, bounds, generic_params } => { + format!( + "{}{}: {}", + pretty_hrtb(generic_params), + pretty_type(type_), + plus_separated(bounds, pretty_generic_bound), + ) + } + WherePredicate::LifetimePredicate { lifetime, outlives } => { + format!("{lifetime}: {}", plus_separated(outlives, identity)) + } + WherePredicate::EqPredicate { lhs, rhs } => { + format!("{} = {}", pretty_type(lhs), pretty_term(rhs)) + } + } +} + +fn pretty_function_header(header: &FunctionHeader) -> String { + let mut result = String::new(); + if header.is_const { + result.push_str("const "); + } + if header.is_unsafe { + result.push_str("unsafe "); + } + if header.is_async { + result.push_str("async "); + } + + let mut abi = |name, unwind: &bool| { + result.push_str(&if *unwind { + format!("extern \"{name}-unwind\" ") + } else { + format!("extern \"{name}\" ") + }) + }; + match &header.abi { + Abi::Rust => {} + Abi::C { unwind } => abi("C", unwind), + Abi::Cdecl { unwind } => abi("Cdecl", unwind), + Abi::Stdcall { unwind } => abi("stdcall", unwind), + Abi::Fastcall { unwind } => abi("fastcall", unwind), + Abi::Aapcs { unwind } => abi("aapcs", unwind), + Abi::Win64 { unwind } => abi("win64", unwind), + Abi::SysV64 { unwind } => abi("sysv", unwind), + Abi::System { unwind } => abi("system", unwind), + Abi::Other(name) => abi(&name, &false), + }; + + result +} + +fn pretty_function_signature(sig: &FunctionSignature) -> String { + let mut result = format!( + "({}", + comma_separated(&sig.inputs, |(name, ty)| format!("{name}: {}", pretty_type(ty))) + ); + if sig.is_c_variadic { + if result.len() > 1 { + result.push_str(", "); + } + result.push_str("..") + } + result.push(')'); + if let Some(output) = &sig.output { + result.push_str(" -> "); + result.push_str(&pretty_type(&output)); + } + result +} + +fn comma_separated(iter: I, func: F) -> String +where + I: IntoIterator, + R: SeparatedInput, + F: Fn(T) -> R, +{ + separated(", ", iter, func) +} + +fn plus_separated(iter: I, func: F) -> String +where + I: IntoIterator, + R: SeparatedInput, + F: Fn(T) -> R, +{ + separated(" + ", iter, func) +} + +fn separated(separator: &str, iter: I, func: F) -> String +where + I: IntoIterator, + R: SeparatedInput, + F: Fn(T) -> R, +{ + let mut result = String::new(); + let mut first = true; + for item in iter { + let item_result = func(item); + let Some(item) = item_result.into_option_str() else { continue }; + if first { + first = false; + } else { + result.push_str(separator); + } + result.push_str(item); + } + result +} + +trait SeparatedInput { + fn into_option_str(&self) -> Option<&str>; +} + +impl SeparatedInput for &str { + fn into_option_str(&self) -> Option<&str> { + Some(self) + } +} + +impl SeparatedInput for String { + fn into_option_str(&self) -> Option<&str> { + Some(self.as_str()) + } +} + +impl SeparatedInput for Option { + fn into_option_str(&self) -> Option<&str> { + self.as_ref().and_then(|s| s.into_option_str()) + } +} + +impl SeparatedInput for &T { + fn into_option_str(&self) -> Option<&str> { + (*self).into_option_str() + } +} diff --git a/src/tools/relnotes-api-list/src/schema.rs b/src/tools/relnotes-api-list/src/schema.rs new file mode 100644 index 0000000000000..c9d37849f4656 --- /dev/null +++ b/src/tools/relnotes-api-list/src/schema.rs @@ -0,0 +1,17 @@ +use serde::Serialize; + +pub(crate) const CURRENT_SCHEMA_VERSION: usize = 1; + +#[derive(Serialize)] +pub(crate) struct Schema { + pub(crate) schema_version: usize, + pub(crate) items: Vec, +} + +#[derive(Serialize)] +pub(crate) struct SchemaItem { + pub(crate) name: String, + pub(crate) url: Option, + pub(crate) deprecated: bool, + pub(crate) children: Vec, +} diff --git a/src/tools/relnotes-api-list/src/stability.rs b/src/tools/relnotes-api-list/src/stability.rs new file mode 100644 index 0000000000000..e9213e03b8c13 --- /dev/null +++ b/src/tools/relnotes-api-list/src/stability.rs @@ -0,0 +1,121 @@ +//! Walk the JSON to determine the stability of every item. +//! +//! There are two ways stability can be assigned to an item: either directly (with the `#[stable]` +//! or `#[unstable]` attributes), or inherited from its parent. The latter case is often used when a +//! `#![unstable]` attribute is applied to the whole module to mark all of its members as unstable. +//! +//! While determining directly assigned stability is trivial and can be done as part of the main +//! conversion visitor, determining inherited stability requires a separate visitor pass. Let's take +//! the following code as an example: +//! +//! ```rust,ignore +//! pub use foo::*; +//! +//! mod foo { +//! #![unstable(..)] +//! +//! pub fn bar() {} +//! pub fn baz() {} +//! } +//! ``` +//! +//! In this case, the conversion pass would visit the `pub use`, follow the import, and then visit +//! `bar()` and `baz()` (without first visiting `foo`). The `pub use` doesn't have its own stability +//! attribute, and even if it had one, it shouldn't be propagated to the items it imports. So, the +//! visitor would not find the stability for `bar()` and `baz()`, even though they are unstable. +//! +//! To solve the problem, we first do a visit of every item to assign its stability, and _then_ we +//! do the final conversion. The stability visit would do nothing when following the import (as +//! there would be no inherited stability), but it would then determine the correct stability by +//! also visiting the `foo` module. + +use crate::store::{Store, StoreCrateId, StoreItem}; +use crate::visitor::{Visitor, walk_item}; +use anyhow::{Error, bail}; +use rustdoc_json_types::{Id, ItemEnum}; +use std::collections::HashMap; +use std::mem::replace; + +#[derive(Debug)] +pub(crate) struct StabilityStore { + result: HashMap<(StoreCrateId, Id), Stability>, +} + +impl StabilityStore { + pub(crate) fn new() -> Self { + Self { result: HashMap::new() } + } + + pub(crate) fn add(&mut self, store: &Store, item: &StoreItem<'_>) -> Result<(), Error> { + StabilityVisitor { store, result: &mut self.result, parent: None }.visit_item(item)?; + Ok(()) + } + + pub(crate) fn get(&self, krate: StoreCrateId, item: Id) -> Option { + self.result.get(&(krate, item)).copied() + } +} + +struct StabilityVisitor<'a, 'b> { + store: &'a Store, + result: &'b mut HashMap<(StoreCrateId, Id), Stability>, + + parent: Option, +} + +impl<'a> Visitor<'a> for StabilityVisitor<'a, '_> { + type Result = (); + + fn visit_item(&mut self, item: &StoreItem<'a>) -> Result<(), Error> { + let mut restore_stability = None; + if let Some(stability) = parse_stability("Stability", &item.attrs)?.or(self.parent) { + self.result.insert((item.crate_id, item.id), stability); + + restore_stability = Some(match &item.inner { + // When we are traversing through an `use` we erase the current stability, as stability + // is not inherited through `use`s. + ItemEnum::Use(_) => replace(&mut self.parent, None), + _ => replace(&mut self.parent, Some(stability)), + }); + } + + walk_item(self, item)?; + + if let Some(restore) = restore_stability { + self.parent = restore; + } + + Ok(()) + } + + fn store(&self) -> &'a Store { + self.store + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum Stability { + Stable, + Unstable, +} + +fn parse_stability( + attribute_name: &str, + attributes: &[String], +) -> Result, Error> { + let attribute_prefix = format!("#[attr = {attribute_name}"); + for attribute in attributes { + if !attribute.starts_with(&attribute_prefix) { + continue; + } + + if attribute.contains("level: Stable") || attribute.contains("level:\nStable") { + return Ok(Some(Stability::Stable)); + } else if attribute.contains("level: Unstable") || attribute.contains("level:\nUnstable") { + return Ok(Some(Stability::Unstable)); + } else { + bail!("couldn't parse stability attribute: {attribute}"); + } + } + Ok(None) +} diff --git a/src/tools/relnotes-api-list/src/store.rs b/src/tools/relnotes-api-list/src/store.rs new file mode 100644 index 0000000000000..5f7b9b2a203aa --- /dev/null +++ b/src/tools/relnotes-api-list/src/store.rs @@ -0,0 +1,159 @@ +use crate::visitor::is_broken_use; +use anyhow::{Error, anyhow, bail}; +use rustdoc_json_types::{Crate, Id, Item, ItemEnum, ItemKind}; +use std::collections::HashMap; +use std::ops::Deref; +use std::path::Path; + +pub(crate) struct Store { + crates: Vec, + names: HashMap, +} + +impl Store { + pub(crate) fn new() -> Self { + Store { crates: Vec::new(), names: HashMap::new() } + } + + pub(crate) fn load(&mut self, path: &Path) -> Result { + let id = StoreCrateId(self.crates.len()); + let json: Crate = serde_json::from_slice(&std::fs::read(path)?)?; + + let name = json + .index + .get(&json.root) + .ok_or_else(|| anyhow!("malformed json: root id is not in the index"))? + .name + .as_deref() + .ok_or_else(|| anyhow!("malformed json: root node doesn't have a name"))? + .to_string(); + + self.names.insert(name.clone(), id); + self.crates.push(StoreCrate { + ids: json + .paths + .iter() + .map(|(id, path)| ((path.kind, path.path.clone()), *id)) + .collect(), + json, + name, + }); + + Ok(id) + } + + pub(crate) fn item(&self, crate_id: StoreCrateId, item: Id) -> Result, Error> { + let krate = + self.crates.get(crate_id.0).ok_or_else(|| anyhow!("missing crate {crate_id:?}"))?; + + Ok(StoreItem { + item: krate + .json + .index + .get(&item) + .ok_or_else(|| anyhow!("missing ID {item:?} in crate {}", krate.name))?, + crate_id, + }) + } + + pub(crate) fn crate_root(&self, id: StoreCrateId) -> Result, Error> { + self.item( + id, + self.crates.get(id.0).ok_or_else(|| anyhow!("missing crate {id:?}"))?.json.root, + ) + } + + pub(crate) fn crate_name(&self, id: StoreCrateId) -> &str { + &self.crates[id.0].name + } + + pub(crate) fn crate_ids(&self) -> impl Iterator { + (0..self.crates.len()).map(|idx| StoreCrateId(idx)) + } + + pub(crate) fn resolve_use( + &self, + krate_id: StoreCrateId, + item: Id, + ) -> Result { + let krate = + self.crates.get(krate_id.0).ok_or_else(|| anyhow!("missing crate {krate_id:?}"))?; + + // External IDs are defined in the `paths` map. Note that crate_id 0 is the current crate. + // If this is a local item just return the same crate and item IDs. + let Some(path) = krate.json.paths.get(&item).filter(|p| p.crate_id != 0) else { + return Ok(UseDefinition { krate: krate_id, item }); + }; + + let extern_summary = krate.json.external_crates.get(&path.crate_id).ok_or_else(|| { + anyhow!("external crate ID {} not present in external_crates", path.crate_id) + })?; + let extern_store_id = self + .names + .get(&extern_summary.name) + .ok_or_else(|| anyhow!("crate {} is not loaded by the too", extern_summary.name))?; + let extern_crate = &self.crates[extern_store_id.0]; + + if let Some(item) = extern_crate.ids.get(&(path.kind, path.path.clone())) { + Ok(UseDefinition { krate: *extern_store_id, item: *item }) + } else { + bail!("could not find item {path:?}"); + } + } + + pub(crate) fn resolve_use_recursive( + &self, + krate_id: StoreCrateId, + item: Id, + ) -> Result { + let mut definition = UseDefinition { krate: krate_id, item }; + loop { + if let ItemEnum::Use(use_) = &self.item(definition.krate, definition.item)?.inner { + if is_broken_use(use_) { + break; + } + if let Some(use_id) = use_.id { + definition = self.resolve_use(definition.krate, use_id)?; + continue; + } + } + break; + } + Ok(definition) + } +} + +struct StoreCrate { + json: Crate, + ids: HashMap<(ItemKind, Vec), Id>, + name: String, +} + +#[derive(Debug)] +pub(crate) struct StoreItem<'a> { + item: &'a Item, + pub(crate) crate_id: StoreCrateId, +} + +impl<'a> StoreItem<'a> { + pub(crate) fn require_name(&self) -> Result<&'a str, Error> { + self.item.name.as_deref().ok_or_else(|| anyhow!("no name for item {:?}", self.item.id)) + } +} + +impl Deref for StoreItem<'_> { + type Target = Item; + + fn deref(&self) -> &Self::Target { + &self.item + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct StoreCrateId(usize); + +#[derive(Debug)] +pub(crate) struct UseDefinition { + pub(crate) krate: StoreCrateId, + pub(crate) item: Id, +} diff --git a/src/tools/relnotes-api-list/src/visitor.rs b/src/tools/relnotes-api-list/src/visitor.rs new file mode 100644 index 0000000000000..74129bcd96894 --- /dev/null +++ b/src/tools/relnotes-api-list/src/visitor.rs @@ -0,0 +1,127 @@ +use crate::store::{Store, StoreCrateId, StoreItem}; +use anyhow::{Context, Error, bail}; +use rustdoc_json_types::{Id, ItemEnum, StructKind, Use}; +use std::iter::once; + +pub(crate) trait Visitor<'a> { + type Result: MergeResults; + + fn store(&self) -> &'a Store; + fn visit_item(&mut self, item: &StoreItem<'a>) -> Result; +} + +pub(crate) fn walk_item<'a, V: Visitor<'a>>( + v: &mut V, + item: &StoreItem<'a>, +) -> Result { + let store = v.store(); + let krate = item.crate_id; + match &item.inner { + ItemEnum::Module(module) => walk_ids(krate, &mut module.items.iter(), v), + ItemEnum::Impl(impl_) => walk_ids(krate, &mut impl_.items.iter(), v), + ItemEnum::Primitive(primitive) => walk_ids(krate, &mut primitive.impls.iter(), v), + ItemEnum::Union(union) => { + walk_ids(krate, &mut union.fields.iter().chain(union.impls.iter()), v) + } + ItemEnum::Enum(enum_) => { + walk_ids(krate, &mut enum_.variants.iter().chain(enum_.impls.iter()), v) + } + ItemEnum::Trait(trait_) => { + // This intentionally doesn't walk through trait_.implementations, as we don't care + // about those when generating the API list. + walk_ids(krate, &mut trait_.items.iter(), v) + } + ItemEnum::Struct(struct_) => match &struct_.kind { + StructKind::Unit => walk_ids(krate, &mut struct_.impls.iter(), v), + StructKind::Tuple(ids) => walk_ids( + krate, + &mut struct_.impls.iter().chain(ids.iter().filter_map(|f| f.as_ref())), + v, + ), + StructKind::Plain { fields, .. } => { + walk_ids(krate, &mut struct_.impls.iter().chain(fields.iter()), v) + } + }, + + ItemEnum::Use(use_) => { + if let Some(used_id) = use_.id { + if is_broken_use(&use_) { + return Ok(V::Result::default()); + } + let dest = + store.resolve_use(krate, used_id).context("could not resolve the use")?; + + if use_.is_glob { + match &store.item(dest.krate, dest.item)?.inner { + ItemEnum::Module(m) => walk_ids(dest.krate, &mut m.items.iter(), v), + ItemEnum::Enum(e) => walk_ids(dest.krate, &mut e.variants.iter(), v), + _ => bail!("glob use doesn't point to a module or enum"), + } + } else { + walk_ids(dest.krate, &mut once(&dest.item), v) + } + } else { + // Do not deal with re-exports of primitives (which have no ID in their use). + Ok(V::Result::default()) + } + } + + ItemEnum::AssocConst { .. } + | ItemEnum::AssocType { .. } + | ItemEnum::ProcMacro(_) + | ItemEnum::Macro(_) + | ItemEnum::ExternType + | ItemEnum::Static(_) + | ItemEnum::Constant { .. } + | ItemEnum::TypeAlias(_) + | ItemEnum::TraitAlias(_) + | ItemEnum::ExternCrate { .. } + | ItemEnum::StructField(_) + | ItemEnum::Variant(_) + | ItemEnum::Function(_) => Ok(V::Result::default()), + } +} + +// TODO: groan +pub(crate) fn is_broken_use(use_: &Use) -> bool { + use_.source.contains("ParseFloatError") + || use_.source == "quote::quote" + || use_.source == "quote::quote_span" +} + +fn walk_ids<'a, 'b, V: Visitor<'a>>( + crate_id: StoreCrateId, + ids: &mut dyn Iterator, + visitor: &mut V, +) -> Result { + let store = visitor.store(); + let mut result = V::Result::default(); + for id in ids { + let item = store.item(crate_id, *id)?; + match visitor.visit_item(&item) { + Ok(new) => result.merge_other(new), + Err(err) => { + let name = match &item.name { + Some(name) => format!("with name {name}"), + None => format!("with ID {id:?}"), + }; + return Err(err.context(format!("while visiting item {name}"))); + } + } + } + Ok(result) +} + +pub(crate) trait MergeResults: Default { + fn merge_other(&mut self, other: Self); +} + +impl MergeResults for () { + fn merge_other(&mut self, _other: Self) {} +} + +impl MergeResults for Vec { + fn merge_other(&mut self, other: Self) { + self.extend(other) + } +} diff --git a/triagebot.toml b/triagebot.toml index 4e3dff219f1d6..5ff5f2430ab19 100644 --- a/triagebot.toml +++ b/triagebot.toml @@ -1250,6 +1250,8 @@ cc = ["@m-ou-se"] [mentions."compiler/rustc_ast_lowering/src/format.rs"] cc = ["@m-ou-se"] +[mentions."src/tools/relnotes-api-list"] +cc = ["@pietroalbini"] # ------------------------------------------------------------------------------ # PR assignments