From 1405665b6134ec9254af91c3e442eb421ec37558 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 22 Dec 2025 10:07:16 +0100 Subject: [PATCH 1/3] Fix documentation links Take documentation links from information in Apple's documentation bundle. We statically know that these are correct. --- Cargo.lock | 1 + crates/header-translator/Cargo.toml | 1 + crates/header-translator/src/config.rs | 6 +- crates/header-translator/src/context.rs | 12 +- crates/header-translator/src/documentation.rs | 148 ++++++++++++++++-- crates/header-translator/src/lib.rs | 1 + crates/header-translator/src/main.rs | 35 ++++- crates/header-translator/src/method.rs | 2 +- crates/header-translator/src/stmt.rs | 41 ++--- generated | 2 +- 10 files changed, 204 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0146dc91..d669d4c24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,6 +771,7 @@ dependencies = [ name = "header-translator" version = "0.1.0" dependencies = [ + "apple-doc", "apple-sdk", "clang", "clang-sys", diff --git a/crates/header-translator/Cargo.toml b/crates/header-translator/Cargo.toml index e4c681257..1c601d9c9 100644 --- a/crates/header-translator/Cargo.toml +++ b/crates/header-translator/Cargo.toml @@ -28,6 +28,7 @@ four-char-code = "2.3.0" regex = "1.6" clap = { version = "4.5.31", features = ["derive"] } itertools = "0.14.0" +apple-doc = { path = "../apple-doc" } [package.metadata.release] release = false diff --git a/crates/header-translator/src/config.rs b/crates/header-translator/src/config.rs index 993b459c2..98cab2a7f 100644 --- a/crates/header-translator/src/config.rs +++ b/crates/header-translator/src/config.rs @@ -487,11 +487,11 @@ impl LibraryConfig { let allowed_in = self.class_data.values(); for data in all.clone().filter(filter_ptr(allowed_in)) { assert_eq!(data.derives, Default::default()); - assert_eq!(data.definition_skipped, Default::default()); + assert_eq!(data.definition_skipped, false); assert_eq!(data.categories, Default::default()); assert_eq!(data.counterpart, Default::default()); assert_eq!(data.skipped_protocols, Default::default()); - assert_eq!(data.main_thread_only, Default::default()); + assert_eq!(data.main_thread_only, false); assert_eq!(data.bridged_to, Default::default()); } @@ -507,7 +507,7 @@ impl LibraryConfig { let allowed_in = self.fns.values(); for data in all.clone().filter(filter_ptr(allowed_in)) { - assert_eq!(data.no_implementor, Default::default()); + assert_eq!(data.no_implementor, false); assert_eq!(data.implementor, Default::default()); assert_eq!(data.arguments, Default::default()); assert_eq!(data.return_, Default::default()); diff --git a/crates/header-translator/src/context.rs b/crates/header-translator/src/context.rs index 8de3f8260..836ac2830 100644 --- a/crates/header-translator/src/context.rs +++ b/crates/header-translator/src/context.rs @@ -5,6 +5,7 @@ use clang::Entity; use proc_macro2::TokenStream; use crate::config::Config; +use crate::documentation::DocState; use crate::expr::Expr; use crate::unexposed_attr::{get_argument_tokens, parse_macro_arguments}; use crate::ItemIdentifier; @@ -77,15 +78,24 @@ pub struct Context<'config> { pub macro_invocations: HashMap, pub ident_mapping: HashMap, pub current_library: &'config str, + pub current_library_title: &'config str, + pub doc: &'config DocState<'config>, } impl<'config> Context<'config> { - pub fn new(config: &'config Config, current_library: &'config str) -> Self { + pub fn new( + config: &'config Config, + current_library: &'config str, + current_library_title: &'config str, + doc: &'config DocState<'config>, + ) -> Self { Self { config, macro_invocations: Default::default(), ident_mapping: Default::default(), current_library, + current_library_title, + doc, } } } diff --git a/crates/header-translator/src/documentation.rs b/crates/header-translator/src/documentation.rs index 1916922d6..d54febbb9 100644 --- a/crates/header-translator/src/documentation.rs +++ b/crates/header-translator/src/documentation.rs @@ -1,20 +1,103 @@ +use std::cell::RefCell; +use std::collections::BTreeMap; use std::fmt::{self, Write as _}; +use apple_doc::{BlobStore, Doc, Kind, SqliteDb}; use clang::documentation::{ BlockCommand, CommentChild, HtmlStartTag, InlineCommand, InlineCommandStyle, ParamCommand, TParamCommand, }; -use clang::Entity; +use clang::{Entity, EntityKind}; use crate::display_helper::FormatterFn; use crate::{Context, ItemIdentifier}; +pub type TxtMap<'a> = BTreeMap<(&'a str, Kind), Vec<&'a str>>; + +pub struct DocState<'data> { + txts: &'data TxtMap<'data>, + sqlite_db: &'data SqliteDb, + blobs: &'data RefCell, +} + +impl<'data> DocState<'data> { + pub fn new( + txts: &'data TxtMap<'data>, + sqlite_db: &'data SqliteDb, + blobs: &'data RefCell, + ) -> Self { + Self { + txts, + sqlite_db, + blobs, + } + } + + pub fn get<'r: 'data, 's: 'r, 'n: 'r>( + &'s self, + name: &'n str, + kind: Kind, + ) -> impl Iterator + 'r { + let ids = self + .txts + .get(&(name, kind)) + .map(|ids| &**ids) + .unwrap_or(&[]); + ids.into_iter().filter_map(move |id| { + // Some entries in `*.txt` don't have a documentation entry. + let r = self.sqlite_db.get_ref(id).unwrap()?; + let mut blobs = self.blobs.borrow_mut(); + Some(blobs.parse_doc(&r).unwrap()) + }) + } + + #[track_caller] + pub fn one_doc(&self, name: &str, kind: Kind, context: &Context<'_>) -> Option { + let mut current = None; + + let mut iter = self.get(name, kind).enumerate().peekable(); + + while let Some((i, doc)) = iter.next() { + // HACK: Use item when there's only one, regardless of the module. + if iter.peek().is_none() && i == 0 { + return Some(doc); + } + + // Remove documentation entries for items in other modules than + // the current module. + // + // TODO: Is there a better way to do this? + if doc.metadata.modules.is_empty() + || doc + .metadata + .modules + .iter() + .any(|module| module.name.as_deref() == Some(context.current_library_title)) + { + if current.is_some() { + error!( + name, + ?kind, + ?current, + ?doc, + "must have only one matching doc item" + ); + } + current = Some(doc); + } + } + + current + } +} + #[derive(Debug, Clone, PartialEq)] pub struct Documentation { first: Option, from_header: Vec, extras: Vec, alias: Option, + apple: Option, } impl Documentation { @@ -24,6 +107,7 @@ impl Documentation { from_header: vec![], extras: vec![], alias: None, + apple: None, } } @@ -53,11 +137,40 @@ impl Documentation { None }; + let txt_kind = match entity.get_kind() { + EntityKind::ObjCInterfaceDecl => Some(Kind::Class), + EntityKind::ObjCCategoryDecl => None, + EntityKind::ObjCProtocolDecl => Some(Kind::Protocol), + EntityKind::TypedefDecl => Some(Kind::Typedef), + EntityKind::StructDecl => Some(Kind::Struct), + EntityKind::UnionDecl => Some(Kind::Union), + EntityKind::EnumDecl => Some(Kind::Enum), + EntityKind::VarDecl => Some(Kind::GlobalVariable), + EntityKind::FunctionDecl => None, // TODO Function + EntityKind::ObjCInstanceMethodDecl => None, // TODO + EntityKind::ObjCPropertyDecl => None, // TODO + EntityKind::ObjCClassMethodDecl => None, // TODO + EntityKind::EnumConstantDecl => Some(Kind::EnumCase), + EntityKind::FieldDecl => None, // TODO + EntityKind::MacroDefinition => None, // TODO + _ => { + warn!(?entity, "unknown entity being documented"); + None + } + }; + + let apple = txt_kind.and_then(|txt_kind| { + entity + .get_name() + .and_then(|c_name| context.doc.one_doc(&c_name, txt_kind, context)) + }); + Self { first: None, from_header, extras: vec![], alias, + apple, } } @@ -73,7 +186,11 @@ impl Documentation { self.alias = Some(alias); } - pub fn fmt<'a>(&'a self, doc_id: Option<&'a ItemIdentifier>) -> impl fmt::Display + 'a { + pub fn set_apple(&mut self, apple: Option) { + self.apple = apple; + } + + pub fn fmt<'a>(&'a self) -> impl fmt::Display + 'a { FormatterFn(move |f| { let mut from_header = String::new(); @@ -89,27 +206,23 @@ impl Documentation { }; // Generate a markdown link to Apple's documentation. - // - // This is best effort only, and doesn't work for functions and - // methods, and possibly some renamed classes and traits. - // - // Additionally, the link may redirect. let mut first = None; let mut last = None; - if let Some(id) = doc_id { - let doc_link = format_args!( - "[Apple's documentation](https://developer.apple.com/documentation/{}/{}?language=objc)", - id.library_name().to_lowercase(), - id.name.to_lowercase() - ); - + if let Some(doc) = &self.apple { + // Output the URL only if we know the true one. + let url_path = doc + .identifier + .url + .strip_prefix("doc://com.apple.documentation") + .unwrap(); + let doc_link = format_args!("https://developer.apple.com{url_path}?language=objc"); if from_header.is_none() && self.first.is_none() { // If there is no documentation, put this as the primary // docs. This looks better in rustdoc. - first = Some(format!("{doc_link}")); + first = Some(format!("[Apple's documentation]({doc_link})")); } else { // Otherwise, put it at the very end. - last = Some(format!("See also {doc_link}")); + last = Some(format!("See also [Apple's documentation]({doc_link})")); } } @@ -341,8 +454,9 @@ mod tests { from_header: children.to_vec(), extras: vec![], alias: None, + apple: None, } - .fmt(None) + .fmt() .to_string(); assert_eq!(actual, expected, "{children:?} was not"); diff --git a/crates/header-translator/src/lib.rs b/crates/header-translator/src/lib.rs index 907e88bd8..8fa3c485a 100644 --- a/crates/header-translator/src/lib.rs +++ b/crates/header-translator/src/lib.rs @@ -36,6 +36,7 @@ pub use self::availability::HOST_MACOS; pub use self::cfgs::PlatformCfg; pub use self::config::{load_config, load_skipped, Config, LibraryConfig}; pub use self::context::{Context, MacroEntity, MacroLocation}; +pub use self::documentation::TxtMap; pub use self::global_analysis::global_analysis; pub use self::id::{ItemIdentifier, Location}; pub use self::library::{EntryExt, Library}; diff --git a/crates/header-translator/src/main.rs b/crates/header-translator/src/main.rs index 8ce9521ce..3c77a1e0d 100644 --- a/crates/header-translator/src/main.rs +++ b/crates/header-translator/src/main.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use std::io::{ErrorKind, Read, Seek, Write}; @@ -7,6 +8,7 @@ use std::{fs, io}; use apple_sdk::{AppleSdk, DeveloperDirectory, Platform, SdkPath, SimpleSdk}; use clang::{Clang, EntityKind, EntityVisitResult, Index, TranslationUnit}; use clap::Parser; +use header_translator::documentation::DocState; use semver::VersionReq; use tracing::{debug_span, error, info, info_span, trace_span}; use tracing_subscriber::filter::LevelFilter; @@ -17,7 +19,8 @@ use tracing_tree::HierarchicalLayer; use header_translator::{ global_analysis, load_config, load_skipped, run_cargo_fmt, Config, Context, EntryExt, Library, - LibraryConfig, Location, MacroEntity, MacroLocation, PlatformCfg, Stmt, HOST_MACOS, VERSION, + LibraryConfig, Location, MacroEntity, MacroLocation, PlatformCfg, Stmt, TxtMap, HOST_MACOS, + VERSION, }; type BoxError = Box; @@ -116,6 +119,22 @@ fn main() -> Result<(), BoxError> { update_root_cargo_toml(workspace_dir, &config); + let _span = info_span!("load documentation helpers").entered(); + let external_dir = apple_doc::external_dir(); + let txt = fs::read_to_string(external_dir.join("1.txt")).unwrap(); // Use ObjC docs + let mut txt_map = TxtMap::new(); + for item in apple_doc::TxtItem::iter(&txt) { + txt_map + .entry((item.name, item.kind)) + .or_default() + .push(item.uuid); + } + let sqlite_db = apple_doc::SqliteDb::from_external_dir(&external_dir).unwrap(); + let blobs = + RefCell::new(apple_doc::BlobStore::from_external_dir(&sqlite_db, &external_dir).unwrap()); + let doc_state = DocState::new(&txt_map, &sqlite_db, &blobs); + drop(_span); + let mut found = false; for (name, data) in config.to_parse() { if let Some(framework) = &framework { @@ -126,7 +145,7 @@ fn main() -> Result<(), BoxError> { found = true; let _span = info_span!("framework", name).entered(); - let library = parse_library(&index, &config, data, name, &sdks, &tempdir); + let library = parse_library(&index, &config, data, name, &sdks, &doc_state, &tempdir); output_library(workspace_dir, name, &library, &config).unwrap(); } if !found { @@ -217,6 +236,7 @@ fn parse_library( data: &LibraryConfig, name: &str, sdks: &[SdkPath], + doc_state: &DocState<'_>, tempdir: &Path, ) -> Library { let mut result = None; @@ -274,10 +294,19 @@ fn parse_library( _ => unimplemented!("SDK platform {sdk:?}"), }; + let mut iter = doc_state.get(name, apple_doc::Kind::Module); + let current_library_title = if let Some(current_library_doc) = iter.next() { + current_library_doc.metadata.title + } else { + name.to_string() + }; + assert_eq!(iter.next(), None); + for llvm_target in llvm_targets { let _span = info_span!("target", platform = ?sdk.platform, llvm_target).entered(); - let mut context = Context::new(config, name); + let mut context = Context::new(config, name, ¤t_library_title, doc_state); + let mut library = Library::new(name, data); let tu = get_translation_unit(index, sdk, llvm_target, data, tempdir); parse_translation_unit(tu, &mut context, &mut library); diff --git a/crates/header-translator/src/method.rs b/crates/header-translator/src/method.rs index b505f50f5..044739c67 100644 --- a/crates/header-translator/src/method.rs +++ b/crates/header-translator/src/method.rs @@ -1109,7 +1109,7 @@ impl fmt::Display for Method { // Attributes // - write!(f, "{}", self.documentation.fmt(None))?; + write!(f, "{}", self.documentation.fmt())?; write!(f, "{}", self.availability)?; if self.must_use { diff --git a/crates/header-translator/src/stmt.rs b/crates/header-translator/src/stmt.rs index e4cabe764..f7e105612 100644 --- a/crates/header-translator/src/stmt.rs +++ b/crates/header-translator/src/stmt.rs @@ -1600,7 +1600,10 @@ impl Stmt { }); }; - let documentation = Documentation::from_entity(&entity, context); + let mut documentation = Documentation::from_entity(&entity, context); + if c_name.is_some() { + documentation.set_apple(None); // TEMPORARY + } if ty.is_simple_uint() { ty = expr.guess_type(id.location()); @@ -2380,7 +2383,7 @@ impl Stmt { let cfg = self.cfg_gate_ln_for([ItemTree::objc("extern_class")], config); write!(f, "{cfg}")?; writeln!(f, "extern_class!(")?; - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, " #[unsafe(super(")?; for (i, (superclass, generics)) in superclasses.iter().enumerate() { if 0 < i { @@ -2508,7 +2511,7 @@ impl Stmt { methods, documentation, } => { - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{availability}")?; write!(f, "{}", self.cfg_gate_ln(config))?; // TODO: Add ?Sized here once `extern_methods!` supports it. @@ -2568,7 +2571,7 @@ impl Stmt { writeln!(f)?; - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{}", self.cfg_gate_ln(config))?; write!(f, "{availability}")?; @@ -2751,7 +2754,7 @@ impl Stmt { write!(f, "{cfg}")?; writeln!(f, "extern_protocol!(")?; - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, " {}", self.cfg_gate_ln(config))?; write!(f, " {availability}")?; if *objc_name != id.name { @@ -2810,7 +2813,7 @@ impl Stmt { documentation, is_union, } => { - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{}", self.cfg_gate_ln(config))?; write!(f, "{availability}")?; @@ -2849,7 +2852,7 @@ impl Stmt { writeln!(f, "pub struct {} {{", id.name)?; } for (name, documentation, ty) in fields { - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; write!(f, " ")?; if name.starts_with('_') { write!(f, "pub(crate) ")?; @@ -2906,7 +2909,7 @@ impl Stmt { sendable, documentation, } => { - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; let mut relevant_enum_cases = variants .iter() @@ -2973,7 +2976,7 @@ impl Stmt { writeln!(f, "impl {} {{", id.name)?; for (name, documentation, availability, expr, _) in variants { - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; let pretty_name = name.strip_prefix(prefix).unwrap_or(name); if pretty_name != name { writeln!(f, " #[doc(alias = \"{name}\")]")?; @@ -3006,7 +3009,7 @@ impl Stmt { writeln!(f, " impl {}: {} {{", id.name, ty.enum_())?; for (name, documentation, availability, expr, _) in variants { - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; let pretty_name = name.strip_prefix(prefix).unwrap_or(name); if pretty_name != name { writeln!(f, " #[doc(alias = \"{name}\")]")?; @@ -3036,7 +3039,7 @@ impl Stmt { writeln!(f, "pub enum {} {{", id.name)?; for (name, documentation, availability, expr, is_zero) in variants { - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; let pretty_name = name.strip_prefix(prefix).unwrap_or(name); if pretty_name != name { writeln!(f, " #[doc(alias = \"{name}\")]")?; @@ -3085,7 +3088,7 @@ impl Stmt { is_last, documentation, } => { - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{}", self.cfg_gate_ln(config))?; write!(f, "{availability}")?; write!(f, "pub const {}: {} = {value};", id.name, ty.const_())?; @@ -3102,7 +3105,7 @@ impl Stmt { documentation, } => { writeln!(f, "extern \"C\" {{")?; - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{}", self.cfg_gate_ln(config))?; write!(f, "{availability}")?; if *link_name != id.name { @@ -3119,7 +3122,7 @@ impl Stmt { value: Some(expr), documentation, } => { - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{}", self.cfg_gate_ln(config))?; write!(f, "{availability}")?; write!(f, "pub static {}: {} = ", id.name, ty.var())?; @@ -3209,7 +3212,7 @@ impl Stmt { }; if needs_wrapper { - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{}", self.cfg_gate_ln(config))?; write!(f, "{availability}")?; if *must_use { @@ -3286,7 +3289,7 @@ impl Stmt { } else { writeln!(f, "{}{{", abi.extern_outer())?; - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; write!(f, " {}", self.cfg_gate_ln(config))?; write!(f, " {availability}")?; if *must_use { @@ -3316,7 +3319,7 @@ impl Stmt { write!(f, "{}", self.cfg_gate_ln(config))?; writeln!(f, "unsafe impl ConcreteType for {} {{", cf_item.id().path())?; - write!(f, "{}", documentation.fmt(None))?; + write!(f, "{}", documentation.fmt())?; writeln!(f, " #[inline]")?; writeln!(f, " {}fn {}(){ret} {{", abi.extern_outer(), id.name)?; @@ -3337,7 +3340,7 @@ impl Stmt { kind, documentation, } => { - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{availability}")?; match kind { Some(UnexposedAttr::TypedEnum) => { @@ -3378,7 +3381,7 @@ impl Stmt { bridged: _, superclass, } => { - write!(f, "{}", documentation.fmt(Some(id)))?; + write!(f, "{}", documentation.fmt())?; write!(f, "{}", self.cfg_gate_ln(config))?; write!(f, "{availability}")?; writeln!(f, "#[repr(C)]")?; diff --git a/generated b/generated index 35654ffc3..7e8f9db06 160000 --- a/generated +++ b/generated @@ -1 +1 @@ -Subproject commit 35654ffc307d2260df68b3bf2d8837d9a3e3bf9a +Subproject commit 7e8f9db06f3d057b4dc1c4bd37d19b0c57e8ac3d From 42d6840a43f50bc860f10bdedad4ee5b201deb93 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 14 Sep 2025 14:57:45 +0200 Subject: [PATCH 2/3] Emit function, macro and enum variant documentation links --- crates/header-translator/src/documentation.rs | 11 ++++++----- crates/header-translator/src/stmt.rs | 3 --- generated | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/header-translator/src/documentation.rs b/crates/header-translator/src/documentation.rs index d54febbb9..8a384b570 100644 --- a/crates/header-translator/src/documentation.rs +++ b/crates/header-translator/src/documentation.rs @@ -146,13 +146,14 @@ impl Documentation { EntityKind::UnionDecl => Some(Kind::Union), EntityKind::EnumDecl => Some(Kind::Enum), EntityKind::VarDecl => Some(Kind::GlobalVariable), - EntityKind::FunctionDecl => None, // TODO Function + EntityKind::FunctionDecl => Some(Kind::Function), EntityKind::ObjCInstanceMethodDecl => None, // TODO - EntityKind::ObjCPropertyDecl => None, // TODO - EntityKind::ObjCClassMethodDecl => None, // TODO + EntityKind::ObjCPropertyDecl => None, // TODO + EntityKind::ObjCClassMethodDecl => None, // TODO EntityKind::EnumConstantDecl => Some(Kind::EnumCase), - EntityKind::FieldDecl => None, // TODO - EntityKind::MacroDefinition => None, // TODO + EntityKind::FieldDecl => None, // TODO + EntityKind::MacroDefinition => Some(Kind::Macro), // TODO + EntityKind::UnexposedDecl => None, _ => { warn!(?entity, "unknown entity being documented"); None diff --git a/crates/header-translator/src/stmt.rs b/crates/header-translator/src/stmt.rs index f7e105612..6f13599c9 100644 --- a/crates/header-translator/src/stmt.rs +++ b/crates/header-translator/src/stmt.rs @@ -1601,9 +1601,6 @@ impl Stmt { }; let mut documentation = Documentation::from_entity(&entity, context); - if c_name.is_some() { - documentation.set_apple(None); // TEMPORARY - } if ty.is_simple_uint() { ty = expr.guess_type(id.location()); diff --git a/generated b/generated index 7e8f9db06..9d5cd9e22 160000 --- a/generated +++ b/generated @@ -1 +1 @@ -Subproject commit 7e8f9db06f3d057b4dc1c4bd37d19b0c57e8ac3d +Subproject commit 9d5cd9e22c634beed11d8b1ab13eeecd748b7ffc From 221b55d9241603434f5d1078e443297910d104af Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 15 Sep 2025 01:42:42 +0200 Subject: [PATCH 3/3] Emit documentation from Apple's documentation bundle --- crates/apple-doc/src/fs_json.rs | 16 +- crates/header-translator/src/documentation.rs | 403 ++++++++++++++++-- crates/header-translator/src/stmt.rs | 2 +- generated | 2 +- 4 files changed, 391 insertions(+), 32 deletions(-) diff --git a/crates/apple-doc/src/fs_json.rs b/crates/apple-doc/src/fs_json.rs index 1cc55b03a..0f472b2af 100644 --- a/crates/apple-doc/src/fs_json.rs +++ b/crates/apple-doc/src/fs_json.rs @@ -206,12 +206,26 @@ pub enum Reference { #[serde(deny_unknown_fields)] pub struct ReferenceVariant { pub url: String, - pub traits: Vec, + pub traits: Vec, #[serde(rename = "svgID")] pub svg_id: Option, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] +pub enum ReferenceVariantTrait { + #[serde(rename = "1x")] + X1, + #[serde(rename = "2x")] + X2, + #[serde(rename = "3x")] + X3, + #[serde(rename = "dark")] + Dark, + #[serde(rename = "light")] + Light, +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] diff --git a/crates/header-translator/src/documentation.rs b/crates/header-translator/src/documentation.rs index 8a384b570..e8704eb08 100644 --- a/crates/header-translator/src/documentation.rs +++ b/crates/header-translator/src/documentation.rs @@ -2,7 +2,10 @@ use std::cell::RefCell; use std::collections::BTreeMap; use std::fmt::{self, Write as _}; -use apple_doc::{BlobStore, Doc, Kind, SqliteDb}; +use apple_doc::{ + BlobStore, Content, Doc, DocKind, Kind, PrimaryContentSection, Reference, ReferenceVariant, + ReferenceVariantTrait, SqliteDb, +}; use clang::documentation::{ BlockCommand, CommentChild, HtmlStartTag, InlineCommand, InlineCommandStyle, ParamCommand, TParamCommand, @@ -193,6 +196,12 @@ impl Documentation { pub fn fmt<'a>(&'a self) -> impl fmt::Display + 'a { FormatterFn(move |f| { + let apple = if let Some(apple) = &self.apple { + format!("{}", apple_to_md(apple)) + } else { + String::new() + }; + let mut from_header = String::new(); for child in &self.from_header { @@ -200,39 +209,21 @@ impl Documentation { } let from_header = fix_code_blocks(&from_header).trim().replace("\t", " "); - let from_header = if from_header.is_empty() { + + let content = if apple.is_empty() && from_header.is_empty() { None + } else if apple == from_header { + // TODO: Merge these instead, by comparing nodes + Some(apple) } else { - Some(from_header.to_string()) + Some(format!("{apple}{from_header}")) }; - // Generate a markdown link to Apple's documentation. - let mut first = None; - let mut last = None; - if let Some(doc) = &self.apple { - // Output the URL only if we know the true one. - let url_path = doc - .identifier - .url - .strip_prefix("doc://com.apple.documentation") - .unwrap(); - let doc_link = format_args!("https://developer.apple.com{url_path}?language=objc"); - if from_header.is_none() && self.first.is_none() { - // If there is no documentation, put this as the primary - // docs. This looks better in rustdoc. - first = Some(format!("[Apple's documentation]({doc_link})")); - } else { - // Otherwise, put it at the very end. - last = Some(format!("See also [Apple's documentation]({doc_link})")); - } - } - - let groups = first + let groups = self + .first .iter() - .chain(self.first.iter()) - .chain(from_header.iter()) - .chain(self.extras.iter()) - .chain(last.iter()); + .chain(content.iter()) + .chain(self.extras.iter()); for (i, group) in groups.enumerate() { if i != 0 { @@ -444,6 +435,360 @@ fn format_child(child: &CommentChild) -> impl fmt::Display + '_ { }) } +/// Format Apple's documentation as markdown link. +fn apple_to_md(doc: &Doc) -> impl fmt::Display + '_ { + FormatterFn(move |f| match &doc.kind { + DocKind::Symbol(page) => { + if !page.abstract_.is_empty() { + writeln!(f, "{}", contents_to_md(doc, &page.abstract_))?; + } + + for section in &doc.sections { + match section { + _ => { + write!(f, "SECTION: {section:?}")?; + } + } + } + + for section in &page.primary_content_sections { + match section { + PrimaryContentSection::Declarations { .. } => {} + PrimaryContentSection::Mentions { .. } => {} + PrimaryContentSection::Content { content } => { + writeln!(f)?; + writeln!(f, "{}", contents_to_md(doc, content))?; + } + PrimaryContentSection::Parameters { parameters } => { + writeln!(f)?; + writeln!(f, "Parameters:")?; + for parameter in parameters { + write!(f, "- ")?; + if let Some(name) = ¶meter.name { + write!(f, "{name}: ")?; + } + write!(f, "{}", contents_to_md(doc, ¶meter.content))?; + } + } + _ => { + write!(f, "PRIMSECTION: {section:?}")?; + } + } + } + + Ok(()) + } + _ => writeln!(f, "TODO doc: {doc:?}"), + }) +} + +fn contents_to_md<'a>(doc: &'a Doc, contents: &'a [Content]) -> impl fmt::Display + 'a { + FormatterFn(move |f| { + for content in contents { + write!(f, "{}", content_to_md(doc, content))?; + } + Ok(()) + }) +} + +fn content_to_md<'a>(doc: &'a Doc, content: &'a Content) -> impl fmt::Display + 'a { + FormatterFn(move |f| match content { + Content::Text { text } => write!(f, "{text}"), + Content::CodeVoice { code } => write!(f, "`{code}`"), + Content::Emphasis { inline_content } => { + write!(f, "_{}_", contents_to_md(doc, inline_content)) + } + Content::Strong { inline_content } => { + write!(f, "**{}**", contents_to_md(doc, inline_content)) + } + Content::ThematicBreak {} => writeln!(f, "---"), + Content::CodeListing { + syntax, + code, + metadata: _, + } => { + write!(f, "```")?; + if let Some(syntax) = syntax { + write!(f, "{syntax}")?; + } else { + write!(f, "text")?; + } + writeln!(f)?; + for line in code { + writeln!(f, "{line}")?; + } + writeln!(f, "```")?; + writeln!(f)?; + Ok(()) + } + Content::Image { + identifier, + metadata, + } => { + if let Some(Reference::Image { variants, alt, .. }) = doc.references.get(identifier) { + let alt = metadata + .as_ref() + .and_then(|metadata| metadata.title.clone()) + .unwrap_or_else(|| alt.clone().unwrap_or_default()); + match &**variants { + [] => { + error!(identifier, "image must have at least one variant"); + Ok(()) + } + [ReferenceVariant { url, .. }] => { + writeln!(f)?; + writeln!(f, "![{alt}]({url})")?; + Ok(()) + } + variants => { + let mut light_srcset = String::new(); + let mut dark_srcset = String::new(); + let mut fallback = None; + + for variant in variants { + let mut pixel_density = ""; + let mut srcset = &mut light_srcset; + + for trait_ in &variant.traits { + match trait_ { + ReferenceVariantTrait::X1 => pixel_density = " 1x", + ReferenceVariantTrait::X2 => pixel_density = " 2x", + ReferenceVariantTrait::X3 => pixel_density = " 3x", + ReferenceVariantTrait::Dark => srcset = &mut dark_srcset, + ReferenceVariantTrait::Light => srcset = &mut light_srcset, + } + } + + if !srcset.is_empty() { + write!(&mut srcset, ", ").unwrap(); + } + write!(&mut srcset, "{}{pixel_density}", variant.url).unwrap(); + + fallback = Some(&variant.url); + } + + writeln!(f)?; + writeln!(f, "")?; + if !dark_srcset.is_empty() { + writeln!( + f, + " " + )?; + } + if !light_srcset.is_empty() { + writeln!( + f, + " " + )?; + } + writeln!(f, " {alt:?}", fallback.unwrap())?; + writeln!(f, "")?; + Ok(()) + } + } + } else { + error!(identifier, "could not find image reference"); + Ok(()) + } + } + Content::Video { identifier, .. } => { + writeln!(f, "(TODO vid: {:?})", doc.references.get(identifier)) + } + Content::Links { style, items } => write!(f, "(TODO links: {content:?})"), + Content::Superscript { inline_content } => { + writeln!(f, "{}", contents_to_md(doc, inline_content)) + } + Content::Small { inline_content } => { + writeln!(f, "{}", contents_to_md(doc, inline_content)) + } + Content::Row { + number_of_columns, + columns, + } => writeln!(f, "(TODO row: {content:?})"), + Content::Table { + header, + extended_data, + rows, + alignments, + metadata, + } => writeln!(f, "(TODO table: {content:?})"), + Content::TabNavigator { tabs } => writeln!(f, "(TODO tabnav: {content:?})"), + Content::UnorderedList { items } => { + for item in items { + write!(f, "-")?; + let mut iter = item.content.iter(); + if let Some(first) = iter.next() { + write!(f, " {}", content_to_md(doc, first))?; + } else { + writeln!(f)?; + } + for content in iter { + write!(f, " {}", content_to_md(doc, content))?; + } + } + Ok(()) + } + Content::OrderedList { start, items } => { + let mut value = start.unwrap_or(1); + let width = (items.len() + value as usize) / 10; + for item in items { + write!(f, "{value:>width$}.")?; + let mut iter = item.content.iter(); + if let Some(first) = iter.next() { + write!(f, " {}", content_to_md(doc, first))?; + } else { + writeln!(f)?; + } + for content in iter { + write!(f, "{:>width$} {}", "", content_to_md(doc, content))?; + } + value += 1; + } + Ok(()) + } + Content::Topic { + identifier, + is_active, + } => writeln!(f, "(TODO topic: {content:?})"), + Content::Reference { + identifier, + is_active, + overriding_title, + overriding_title_inline_content, + } => { + let reference = doc + .references + .get(identifier) + .expect("must have reference in doc"); + write!( + f, + "{}", + reference_to_md( + doc, + reference, + overriding_title.as_ref(), + overriding_title_inline_content.as_ref() + ) + ) + } + Content::Paragraph { inline_content } => { + writeln!(f, "{}", contents_to_md(doc, inline_content))?; + writeln!(f)?; + Ok(()) + } + Content::Heading { + anchor, + level, + text, + } => { + // Remove anchor if it's (likely) what `rustdoc` is going to output. + // https://github.com/rust-lang/rust/blob/ddaf12390d3ffb7d5ba74491a48f3cd528e5d777/src/librustdoc/html/markdown.rs#L571 + // + // TODO: Normalize the anchor (lowercase). + let anchor = anchor.to_ascii_lowercase(); + if text.chars().filter_map(slugify).collect::() != anchor { + writeln!(f, "")?; + } + for _ in 0..*level { + write!(f, "#")?; + } + writeln!(f, " {text}")?; + writeln!(f)?; + Ok(()) + } + Content::NewTerm { inline_content } => { + writeln!(f, "(TODO newterm: {})", contents_to_md(doc, inline_content))?; + Ok(()) + } + Content::Aside { + name, + // Styles include "warning", "important", "note" and "tip". + // Only "warning" is supported by Rustdoc though: + // https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html#adding-a-warning-block + style: _, + content, + } => { + writeln!(f, "
")?; + writeln!(f)?; + + writeln!(f, "### {}", name.as_deref().unwrap_or("Aside"))?; + + writeln!(f, "{}", contents_to_md(doc, content))?; + + writeln!(f)?; + writeln!(f, "
")?; + + Ok(()) + } + Content::TermList { items } => { + for term in items { + write!( + f, + "- {}: {}", + contents_to_md(doc, &term.term.inline_content), + contents_to_md(doc, &term.definition.content) + )?; + } + + Ok(()) + } + }) +} + +fn reference_to_md<'a>( + doc: &'a Doc, + reference: &'a Reference, + overriding_title: Option<&'a String>, + overriding_title_inline_content: Option<&'a Vec>, +) -> impl fmt::Display + 'a { + FormatterFn(move |f| match reference { + Reference::Link { + identifier: _, + url, + title: _, + title_inline_content, + } => { + let title_inline_content = + overriding_title_inline_content.unwrap_or(title_inline_content); + write!(f, "[{}]({url})", contents_to_md(doc, title_inline_content)) + } + Reference::Topic { + identifier: _, + kind, + title, + name: _, + title_style: _, + url, + .. + } => { + let title = overriding_title.unwrap_or(title); + // TODO: Make these a doc link to the actual item. + // + // Probably requires using the identifier to look up the item. + if kind == "symbol" { + write!(f, "[`{title}`](https://developer.apple.com{url})") + } else { + write!(f, "[{title}](https://developer.apple.com{url})") + } + } + _ => write!(f, "REFERENCE TODO: {reference:?}"), + }) +} + +fn slugify(c: char) -> Option { + if c.is_alphanumeric() || c == '-' || c == '_' { + if c.is_ascii() { + Some(c.to_ascii_lowercase()) + } else { + Some(c) + } + } else if c.is_whitespace() && c.is_ascii() { + Some('-') + } else { + None + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/header-translator/src/stmt.rs b/crates/header-translator/src/stmt.rs index 6f13599c9..4b28537df 100644 --- a/crates/header-translator/src/stmt.rs +++ b/crates/header-translator/src/stmt.rs @@ -1600,7 +1600,7 @@ impl Stmt { }); }; - let mut documentation = Documentation::from_entity(&entity, context); + let documentation = Documentation::from_entity(&entity, context); if ty.is_simple_uint() { ty = expr.guess_type(id.location()); diff --git a/generated b/generated index 9d5cd9e22..ac897bb04 160000 --- a/generated +++ b/generated @@ -1 +1 @@ -Subproject commit 9d5cd9e22c634beed11d8b1ab13eeecd748b7ffc +Subproject commit ac897bb04ac0f8fd697f84995859e449b8ec4377