diff --git a/src/adapter/edges.rs b/src/adapter/edges.rs index 830d2a65..2bd328cc 100644 --- a/src/adapter/edges.rs +++ b/src/adapter/edges.rs @@ -325,6 +325,35 @@ pub(super) fn resolve_enum_edge<'a, V: AsVertex> + 'a>( } } +pub(super) fn resolve_union_edge<'a, V: AsVertex> + 'a>( + contexts: ContextIterator<'a, V>, + edge_name: &str, + current_crate: &'a IndexedCrate<'a>, + previous_crate: Option<&'a IndexedCrate<'a>>, +) -> ContextOutcomeIterator<'a, V, VertexIterator<'a, Vertex<'a>>> { + match edge_name { + "field" => resolve_neighbors_with(contexts, move |vertex| { + let origin = vertex.origin; + let union_item = vertex.as_union().expect("vertex was not an Union"); + + let item_index = match origin { + Origin::CurrentCrate => ¤t_crate.inner.index, + Origin::PreviousCrate => { + &previous_crate + .expect("no previous crate provided") + .inner + .index + } + }; + + Box::new(union_item.fields.iter().map(move |field_id| { + origin.make_item_vertex(item_index.get(field_id).expect("missing item")) + })) + }), + _ => unreachable!("resolve_union_edge {edge_name}"), + } +} + pub(super) fn resolve_struct_field_edge<'a, V: AsVertex> + 'a>( contexts: ContextIterator<'a, V>, edge_name: &str, diff --git a/src/adapter/mod.rs b/src/adapter/mod.rs index 6f96c455..29569e0c 100644 --- a/src/adapter/mod.rs +++ b/src/adapter/mod.rs @@ -92,8 +92,8 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { "Crate" => properties::resolve_crate_property(contexts, property_name), "Item" => properties::resolve_item_property(contexts, property_name), "ImplOwner" | "Struct" | "StructField" | "Enum" | "Variant" | "PlainVariant" - | "TupleVariant" | "StructVariant" | "Trait" | "Function" | "Method" | "Impl" - | "GlobalValue" | "Constant" | "Static" | "AssociatedType" + | "TupleVariant" | "StructVariant" | "Union" | "Trait" | "Function" | "Method" + | "Impl" | "GlobalValue" | "Constant" | "Static" | "AssociatedType" | "AssociatedConstant" | "Module" if matches!( property_name.as_ref(), @@ -113,6 +113,7 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { "Module" => properties::resolve_module_property(contexts, property_name), "Struct" => properties::resolve_struct_property(contexts, property_name), "Enum" => properties::resolve_enum_property(contexts, property_name), + "Union" => properties::resolve_union_property(contexts, property_name), "Span" => properties::resolve_span_property(contexts, property_name), "Path" => properties::resolve_path_property(contexts, property_name), "ImportablePath" => { @@ -165,7 +166,7 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { match type_name.as_ref() { "CrateDiff" => edges::resolve_crate_diff_edge(contexts, edge_name), "Crate" => edges::resolve_crate_edge(self, contexts, edge_name, resolve_info), - "Importable" | "ImplOwner" | "Struct" | "Enum" | "Trait" | "Function" + "Importable" | "ImplOwner" | "Struct" | "Enum" | "Union" | "Trait" | "Function" | "GlobalValue" | "Constant" | "Static" | "Module" if matches!(edge_name.as_ref(), "importable_path" | "canonical_path") => { @@ -177,14 +178,14 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { ) } "Item" | "ImplOwner" | "Struct" | "StructField" | "Enum" | "Variant" - | "PlainVariant" | "TupleVariant" | "StructVariant" | "Trait" | "Function" - | "Method" | "Impl" | "GlobalValue" | "Constant" | "Static" | "AssociatedType" - | "AssociatedConstant" | "Module" + | "PlainVariant" | "TupleVariant" | "Union" | "StructVariant" | "Trait" + | "Function" | "Method" | "Impl" | "GlobalValue" | "Constant" | "Static" + | "AssociatedType" | "AssociatedConstant" | "Module" if matches!(edge_name.as_ref(), "span" | "attribute") => { edges::resolve_item_edge(contexts, edge_name) } - "ImplOwner" | "Struct" | "Enum" + "ImplOwner" | "Struct" | "Enum" | "Union" if matches!(edge_name.as_ref(), "impl" | "inherent_impl") => { edges::resolve_impl_owner_edge(self, contexts, edge_name, resolve_info) @@ -220,6 +221,12 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { self.current_crate, self.previous_crate, ), + "Union" => edges::resolve_union_edge( + contexts, + edge_name, + self.current_crate, + self.previous_crate, + ), "StructField" => edges::resolve_struct_field_edge(contexts, edge_name), "Impl" => edges::resolve_impl_edge(self, contexts, edge_name, resolve_info), "Trait" => edges::resolve_trait_edge( @@ -254,7 +261,7 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { actual_type_name, "PlainVariant" | "TupleVariant" | "StructVariant" ), - "ImplOwner" => matches!(actual_type_name, "Struct" | "Enum"), + "ImplOwner" => matches!(actual_type_name, "Struct" | "Enum" | "Union"), "GlobalValue" => matches!(actual_type_name, "Constant" | "Static",), _ => { // The remaining types are final (don't have any subtypes) @@ -277,6 +284,7 @@ pub(crate) fn supported_item_kind(item: &Item) -> bool { | rustdoc_types::ItemEnum::StructField(..) | rustdoc_types::ItemEnum::Enum(..) | rustdoc_types::ItemEnum::Variant(..) + | rustdoc_types::ItemEnum::Union(..) | rustdoc_types::ItemEnum::Function(..) | rustdoc_types::ItemEnum::Impl(..) | rustdoc_types::ItemEnum::Trait(..) diff --git a/src/adapter/optimizations/impl_lookup.rs b/src/adapter/optimizations/impl_lookup.rs index 5167a59f..d8862486 100644 --- a/src/adapter/optimizations/impl_lookup.rs +++ b/src/adapter/optimizations/impl_lookup.rs @@ -189,13 +189,14 @@ fn resolve_owner_impl_slow_path<'a>( }; // Get the IDs of all the impl blocks. - // Relies on the fact that only structs and enums can have impls, - // so we know that the vertex must represent either a struct or an enum. + // Relies on the fact that only structs, enums, and unions can have impls, + // so we know that the vertex must represent either a struct, enum, or union. let impl_ids = vertex .as_struct() .map(|s| &s.impls) .or_else(|| vertex.as_enum().map(|e| &e.impls)) - .expect("vertex was neither a struct nor an enum"); + .or_else(|| vertex.as_union().map(|u| &u.impls)) + .expect("vertex was neither a struct, enum, or union"); Box::new(impl_ids.iter().filter_map(move |item_id| { let next_item = item_index.get(item_id); diff --git a/src/adapter/properties.rs b/src/adapter/properties.rs index 4393a0d9..bf3c15c4 100644 --- a/src/adapter/properties.rs +++ b/src/adapter/properties.rs @@ -170,6 +170,18 @@ pub(super) fn resolve_enum_property<'a, V: AsVertex> + 'a>( } } +pub(super) fn resolve_union_property<'a, V: AsVertex> + 'a>( + contexts: ContextIterator<'a, V>, + property_name: &str, +) -> ContextOutcomeIterator<'a, V, FieldValue> { + match property_name { + "fields_stripped" => { + resolve_property_with(contexts, field_property!(as_union, fields_stripped)) + } + _ => unreachable!("Union property {property_name}"), + } +} + pub(super) fn resolve_path_property<'a, V: AsVertex> + 'a>( contexts: ContextIterator<'a, V>, property_name: &str, diff --git a/src/adapter/tests.rs b/src/adapter/tests.rs index aa01103e..8dff6e93 100644 --- a/src/adapter/tests.rs +++ b/src/adapter/tests.rs @@ -1392,3 +1392,268 @@ fn trait_associated_items_public_api_eligible() { results ); } + +#[test] +fn unions() { + let path = "./localdata/test_data/unions/rustdoc.json"; + let content = std::fs::read_to_string(path) + .with_context(|| format!("Could not load {path} file, did you forget to run ./scripts/regenerate_test_rustdocs.sh ?")) + .expect("failed to load rustdoc"); + + let crate_ = serde_json::from_str(&content).expect("failed to parse rustdoc"); + let indexed_crate = IndexedCrate::new(&crate_); + let adapter = Arc::new(RustdocAdapter::new(&indexed_crate, None)); + + // Part 1: make sure unions have correct visibility (similart to importable_paths + // test case) + + let query = r#" +{ + Crate { + item { + ... on Union { + name @output + importable_path { + path @output + doc_hidden @output + deprecated @output + public_api @output + } + } + } + } +} +"#; + + let variables: BTreeMap<&str, &str> = BTreeMap::default(); + + let schema = + Schema::parse(include_str!("../rustdoc_schema.graphql")).expect("schema failed to parse"); + + #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize)] + struct Output { + name: String, + path: Vec, + doc_hidden: bool, + deprecated: bool, + public_api: bool, + } + + let mut results: Vec<_> = + trustfall::execute_query(&schema, adapter.clone(), query, variables.clone()) + .expect("failed to run query") + .map(|row| row.try_into_struct().expect("shape mismatch")) + .collect(); + results.sort_unstable(); + + // We write the results in the order the items appear in the test file, + // and sort them afterward in order to compare with the (sorted) query results. + // This makes it easier to verify that the expected data here is correct + // by reading it side-by-side with the file. + let mut expected_results = vec![ + Output { + name: "PublicImportable".into(), + path: vec!["unions".into(), "PublicImportable".into()], + doc_hidden: false, + deprecated: false, + public_api: true, + }, + Output { + name: "ModuleHidden".into(), + path: vec!["unions".into(), "hidden".into(), "ModuleHidden".into()], + doc_hidden: true, + deprecated: false, + public_api: false, + }, + Output { + name: "DeprecatedModuleHidden".into(), + path: vec![ + "unions".into(), + "hidden".into(), + "DeprecatedModuleHidden".into(), + ], + doc_hidden: true, + deprecated: true, + public_api: true, + }, + Output { + name: "ModuleDeprecatedModuleHidden".into(), + path: vec![ + "unions".into(), + "hidden".into(), + "deprecated".into(), + "ModuleDeprecatedModuleHidden".into(), + ], + doc_hidden: true, + deprecated: true, + public_api: true, + }, + Output { + name: "Hidden".into(), + path: vec!["unions".into(), "submodule".into(), "Hidden".into()], + doc_hidden: true, + deprecated: false, + public_api: false, + }, + Output { + name: "DeprecatedHidden".into(), + path: vec![ + "unions".into(), + "submodule".into(), + "DeprecatedHidden".into(), + ], + doc_hidden: true, + deprecated: true, + public_api: true, + }, + Output { + name: "ModuleDeprecated".into(), + path: vec![ + "unions".into(), + "deprecated".into(), + "ModuleDeprecated".into(), + ], + doc_hidden: false, + deprecated: true, + public_api: true, + }, + Output { + name: "ModuleDeprecatedHidden".into(), + path: vec![ + "unions".into(), + "deprecated".into(), + "ModuleDeprecatedHidden".into(), + ], + doc_hidden: true, + deprecated: true, + public_api: true, + }, + Output { + name: "ModuleHidden".into(), + path: vec!["unions".into(), "UsedVisible".into()], + doc_hidden: false, + deprecated: false, + public_api: true, + }, + Output { + name: "Hidden".into(), + path: vec!["unions".into(), "UsedHidden".into()], + doc_hidden: true, + deprecated: false, + public_api: false, + }, + Output { + name: "ModuleDeprecated".into(), + path: vec!["unions".into(), "UsedModuleDeprecated".into()], + doc_hidden: false, + deprecated: true, + public_api: true, + }, + Output { + name: "ModuleDeprecatedHidden".into(), + path: vec!["unions".into(), "UsedModuleDeprecatedHidden".into()], + doc_hidden: true, + deprecated: true, + public_api: true, + }, + Output { + name: "PublicImportable".into(), + path: vec![ + "unions".into(), + "reexports".into(), + "DeprecatedReexport".into(), + ], + doc_hidden: false, + deprecated: true, + public_api: true, + }, + Output { + name: "PublicImportable".into(), + path: vec!["unions".into(), "reexports".into(), "HiddenReexport".into()], + doc_hidden: true, + deprecated: false, + public_api: false, + }, + Output { + name: "ModuleDeprecated".into(), + path: vec![ + "unions".into(), + "reexports".into(), + "HiddenDeprecatedReexport".into(), + ], + doc_hidden: true, + deprecated: true, + public_api: true, + }, + ]; + expected_results.sort_unstable(); + + similar_asserts::assert_eq!(expected_results, results); + + // Part 2: make sure union data is properly queryable + + let query = r#" +{ + Crate { + item { + ... on Module { + name @filter(op: "=", value: ["$data"]) + + item { + ... on Union { + union_name: name @output + field @fold { + visibility_limit @filter(op: "=", value: ["$public"]) + name @output + raw_type { + type_name: name @output + } + } + } + } + } + } + } +}"#; + + let variables: BTreeMap<&str, &str> = btreemap! { "data" => "data" , "public" => "public"}; + + #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize)] + struct FieldInfo { + union_name: String, + name: Vec, + type_name: Vec, + } + + let mut results: Vec<_> = + trustfall::execute_query(&schema, adapter.clone(), query, variables.clone()) + .expect("failed to run query") + .map(|row| row.try_into_struct::().expect("shape mismatch")) + .collect(); + results.sort_unstable(); + + // We write the results in the order the items appear in the test file, + // and sort them afterward in order to compare with the (sorted) query results. + // This makes it easier to verify that the expected data here is correct + // by reading it side-by-side with the file. + let mut expected_results = vec![ + FieldInfo { + union_name: "NoFieldsPublic".into(), + name: vec![], + type_name: vec![], + }, + FieldInfo { + union_name: "SomeFieldsPublic".into(), + name: vec!["y".into()], + type_name: vec!["f32".into()], + }, + FieldInfo { + union_name: "AllFieldsPublic".into(), + name: vec!["x".into(), "y".into()], + type_name: vec!["usize".into(), "f32".into()], + }, + ]; + expected_results.sort_unstable(); + + similar_asserts::assert_eq!(expected_results, results); +} diff --git a/src/adapter/vertex.rs b/src/adapter/vertex.rs index abb6d6e6..94fc48be 100644 --- a/src/adapter/vertex.rs +++ b/src/adapter/vertex.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use rustdoc_types::{ Abi, Constant, Crate, Enum, Function, Impl, Item, Module, Path, Span, Static, Struct, Trait, - Type, Variant, VariantKind, + Type, Union, Variant, VariantKind, }; use trustfall::provider::Typename; @@ -48,6 +48,7 @@ impl<'a> Typename for Vertex<'a> { rustdoc_types::ItemEnum::Module { .. } => "Module", rustdoc_types::ItemEnum::Struct(..) => "Struct", rustdoc_types::ItemEnum::Enum(..) => "Enum", + rustdoc_types::ItemEnum::Union(..) => "Union", rustdoc_types::ItemEnum::Function(..) => "Function", rustdoc_types::ItemEnum::Variant(variant) => match variant.kind { VariantKind::Plain => "PlainVariant", @@ -149,6 +150,13 @@ impl<'a> Vertex<'a> { }) } + pub(super) fn as_union(&self) -> Option<&'a Union> { + self.as_item().and_then(|item| match &item.inner { + rustdoc_types::ItemEnum::Union(u) => Some(u), + _ => None, + }) + } + pub(super) fn as_trait(&self) -> Option<&'a Trait> { self.as_item().and_then(|item| match &item.inner { rustdoc_types::ItemEnum::Trait(t) => Some(t), diff --git a/src/indexed_crate.rs b/src/indexed_crate.rs index a92f3374..51236fb9 100644 --- a/src/indexed_crate.rs +++ b/src/indexed_crate.rs @@ -829,6 +829,9 @@ mod tests { "Baz" => btreeset![ "glob_of_glob_reexport::Baz", ], + "Onion" => btreeset![ + "glob_of_glob_reexport::Onion", + ], }; assert_exported_items_match(test_crate, &expected_items); @@ -847,6 +850,9 @@ mod tests { "First" => btreeset![ "glob_of_renamed_reexport::RenamedFirst", ], + "Onion" => btreeset![ + "glob_of_renamed_reexport::RenamedOnion", + ], }; assert_exported_items_match(test_crate, &expected_items); diff --git a/src/rustdoc_schema.graphql b/src/rustdoc_schema.graphql index fc5c4978..494e7522 100644 --- a/src/rustdoc_schema.graphql +++ b/src/rustdoc_schema.graphql @@ -593,6 +593,91 @@ type StructVariant implements Item & Variant { field: [StructField!] } +""" +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Item.html +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/enum.ItemEnum.html +https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Union.html +""" +type Union implements Item & Importable & ImplOwner { + # properties from Item + id: String! + crate_id: Int! + name: String + docs: String + attrs: [String!]! + + """ + This item is hidden in documentation. + """ + doc_hidden: Boolean! + + """ + This item is deprecated. + """ + deprecated: Boolean! + + """ + Whether this item is eligible to be in the public API. This is true if both: + - The item is public, either explicitly (`pub`) or implicitly (like enum variants). + - The item is visible in documentation, or is not visible but is deprecated, + since deprecated items are assumed to have been part of the public API in the past. + + Being eligible to be part of the public API *does not* make an item public API! + An item that is not eligible by itself cannot be part of the public API, + but eligible items might not be public API -- for example, pub-in-priv items + (public items in a private module) are eligible but not public API. Another example + would be a public item that is only reachable via modules that are both + `#[doc(hidden)]` and not deprecated. + + To check whether an item is part of the public API: + - For items that are legal in a `use` import statement, use the `importable_path` edge and the + path's `public_api` property. + - For all other items (e.g. struct fields), first check whether that parent item is public API + as above, and then check whether the item itself is `public_api_eligible`. + """ + public_api_eligible: Boolean! + + visibility_limit: String! + + # own properties + fields_stripped: Boolean! + + # edges from Item + span: Span + attribute: [Attribute!] + + # edges from Importable + importable_path: [ImportablePath!] + canonical_path: Path + + # edges from ImplOwner + """ + Any impl for this type. + + All impl kinds are included: + - inherent impls: `impl Foo` + - explicit trait implementations: `impl Bar for Foo` + - blanket implementations: `impl Bar for T` + """ + impl: [Impl!] + + """ + Only inherent impls: implementations of the type itself (`impl Foo`). + + The impls pointed to here are guaranteed to have no `trait` and no `blanket` edges. + + This edge is just a convenience to simplify query-writing, + so we don't have to keep writing "@fold @transform(...) @filter(...)" chains + over the `trait` and `blanket` edges. + + When Trustfall supports macro edges, this should just become a macro edge. + """ + inherent_impl: [Impl!] + + # own edges + field: [StructField!] +} + """ https://docs.rs/rustdoc-types/0.11.0/rustdoc_types/struct.Span.html """ diff --git a/test_crates/glob_of_glob_reexport/src/lib.rs b/test_crates/glob_of_glob_reexport/src/lib.rs index 2676055a..9285e065 100644 --- a/test_crates/glob_of_glob_reexport/src/lib.rs +++ b/test_crates/glob_of_glob_reexport/src/lib.rs @@ -2,6 +2,7 @@ //! - `foo` //! - `Bar` //! - `Baz` +//! - `Onion` mod nested { mod deeper { @@ -12,6 +13,10 @@ mod nested { pub enum Baz { First, } + + pub union Onion { + pub field: usize + } } pub(crate) mod sibling { diff --git a/test_crates/glob_of_renamed_reexport/src/lib.rs b/test_crates/glob_of_renamed_reexport/src/lib.rs index effc07fe..ddc351a3 100644 --- a/test_crates/glob_of_renamed_reexport/src/lib.rs +++ b/test_crates/glob_of_renamed_reexport/src/lib.rs @@ -2,6 +2,7 @@ //! - `renamed_foo` //! - `RenamedBar` //! - `RenamedFirst` +//! - `RenamedOnion` mod nested { mod deeper { @@ -12,12 +13,17 @@ mod nested { pub enum Baz { First, } + + pub union Onion { + pub field: usize + } } pub(crate) mod renaming { pub use super::deeper::foo as renamed_foo; pub use super::deeper::Bar as RenamedBar; pub use super::deeper::Baz::First as RenamedFirst; + pub use super::deeper::Onion as RenamedOnion; } } diff --git a/test_crates/importable_paths/src/lib.rs b/test_crates/importable_paths/src/lib.rs index 950bc7b6..83a8b15b 100644 --- a/test_crates/importable_paths/src/lib.rs +++ b/test_crates/importable_paths/src/lib.rs @@ -19,6 +19,10 @@ mod private { Hidden, } + union PrivateUnion { + foo: usize + } + trait SomeTrait { #[doc(hidden)] #[deprecated] diff --git a/test_crates/unions/Cargo.toml b/test_crates/unions/Cargo.toml new file mode 100644 index 00000000..db406dd4 --- /dev/null +++ b/test_crates/unions/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "unions" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/test_crates/unions/src/lib.rs b/test_crates/unions/src/lib.rs new file mode 100644 index 00000000..81d9b1aa --- /dev/null +++ b/test_crates/unions/src/lib.rs @@ -0,0 +1,114 @@ +// Unions have their own special test suite because they were added after structs +// and enums. +// +// Part 1: unions import correctly. Largely derived from the importable paths +// test crate. + +pub union PublicImportable { + x: usize, +} + +mod private { + pub union PubInPriv { + x: usize, + } + + union Private { + x: usize, + } + + union PrivateUnion { + foo: usize, + } +} + +#[doc(hidden)] +pub mod hidden { + pub union ModuleHidden { + x: usize, + } + + #[deprecated] + pub union DeprecatedModuleHidden { + x: usize, + } // public_api + + #[deprecated] + pub mod deprecated { + pub union ModuleDeprecatedModuleHidden { + x: usize, + } // public_api + } +} + +pub mod submodule { + #[doc(hidden)] + pub union Hidden { + x: usize, + } + + #[deprecated] + #[doc(hidden)] + pub union DeprecatedHidden { + x: usize, + } // public_api +} + +#[deprecated] +pub mod deprecated { + pub union ModuleDeprecated { + x: usize, + } // public_api + + #[doc(hidden)] + pub union ModuleDeprecatedHidden { + x: usize, + } // public_api +} + +// This is expected to be visible in rustdoc. +pub use hidden::ModuleHidden as UsedVisible; // public_api + +// This is expected to be hidden in rustdoc. +pub use submodule::Hidden as UsedHidden; + +// This is expected to be public_api and deprecated +pub use deprecated::ModuleDeprecated as UsedModuleDeprecated; + +// Still public_api, the item is deprecated (via its module) so the item is visible. +pub use deprecated::ModuleDeprecatedHidden as UsedModuleDeprecatedHidden; + +pub mod reexports { + // Re-exports can be deprecated too. + #[deprecated] + pub use super::PublicImportable as DeprecatedReexport; + + // Re-exports can be doc-hidden as well. + #[doc(hidden)] + pub use super::PublicImportable as HiddenReexport; + + // Doc-hidden re-exports of deprecated items are still public API. + #[doc(hidden)] + pub use super::deprecated::ModuleDeprecated as HiddenDeprecatedReexport; +} + +// Part 2: union data/info querying is correct + +mod data { + use std::mem::ManuallyDrop; + + pub union NoFieldsPublic { + x: usize, + y: f32, + } + + pub union SomeFieldsPublic { + x: usize, + pub y: f32, + } + + pub union AllFieldsPublic { + pub x: usize, + pub y: f32, + } +}