From e3c493d1a8e37b191251958fadfbfc9f73cf98a1 Mon Sep 17 00:00:00 2001 From: Predrag Gruevski <2348618+obi1kenobi@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:55:14 -0500 Subject: [PATCH] Allow `Impl` blocks to have their own generic params and bounds. (#560) (#563) For example: ```rust struct Example { value: T, } impl Example { const N: usize = 42; } ``` In this case the inherent `impl` block defines an item that *only* exists if the `T` is `Debug`. The schema and implementation changes in this PR make it possible to query for that information. --- src/adapter/edges.rs | 21 ++++- src/adapter/mod.rs | 1 + src/adapter/origin.rs | 9 +- src/adapter/properties.rs | 20 ++-- src/adapter/rust_type_name.rs | 34 ++++--- src/adapter/tests.rs | 127 ++++++++++++++++++++++++++ src/adapter/vertex.rs | 26 ++++-- src/rustdoc_schema.graphql | 9 +- test_crates/rust_type_name/src/lib.rs | 12 +++ 9 files changed, 221 insertions(+), 38 deletions(-) diff --git a/src/adapter/edges.rs b/src/adapter/edges.rs index 30234ec0..c52302f0 100644 --- a/src/adapter/edges.rs +++ b/src/adapter/edges.rs @@ -525,7 +525,7 @@ pub(super) fn resolve_impl_edge<'a, V: AsVertex> + 'a>( }); Box::new(std::iter::once( - origin.make_implemented_trait_vertex(path, found_item), + origin.make_implemented_trait_vertex(path, None, found_item), )) } else { Box::new(std::iter::empty()) @@ -599,7 +599,13 @@ pub(super) fn resolve_trait_edge<'a, V: AsVertex> + 'a>( manually_inlined_builtin_traits.get(&trait_.id) }); - Some(origin.make_implemented_trait_vertex(trait_, found_item)) + // TODO: Remove this once rust-analyzer stops falsely inferring the type of + // `bound` as `GenericBound` when in fact it's `&GenericBound`. + // It shows a phantom compile error unless we add `&` before `bound`. + #[allow(clippy::needless_borrow)] + let trait_bound: Option<&rustdoc_types::GenericBound> = Some(&bound); + + Some(origin.make_implemented_trait_vertex(trait_, trait_bound, found_item)) } else { None } @@ -693,12 +699,13 @@ pub(super) fn resolve_implemented_trait_edge<'a, V: AsVertex> + 'a>( "trait" => resolve_neighbors_with(contexts, move |vertex| { let origin = vertex.origin; - let (_, trait_item) = vertex + let impld_trait = vertex .as_implemented_trait() .expect("vertex was not an ImplementedTrait"); Box::new( - trait_item + impld_trait + .resolved_item .into_iter() .map(move |item| origin.make_item_vertex(item)), ) @@ -861,7 +868,11 @@ pub(super) fn resolve_generic_type_parameter_edge<'a, V: AsVertex> + manually_inlined_builtin_traits.get(&trait_.id) }); - Some(origin.make_implemented_trait_vertex(trait_, found_item)) + Some(origin.make_implemented_trait_vertex( + trait_, + Some(bound), + found_item, + )) } else { None } diff --git a/src/adapter/mod.rs b/src/adapter/mod.rs index 6d36541c..8648a7a2 100644 --- a/src/adapter/mod.rs +++ b/src/adapter/mod.rs @@ -276,6 +276,7 @@ impl<'a> Adapter<'a> for RustdocAdapter<'a> { edges::resolve_function_like_edge(contexts, edge_name) } "GenericItem" | "Struct" | "Enum" | "Union" | "Trait" | "Function" | "Method" + | "Impl" if matches!(edge_name.as_ref(), "generic_parameter") => { edges::resolve_generic_parameter_edge(contexts, edge_name) diff --git a/src/adapter/origin.rs b/src/adapter/origin.rs index fc981278..e9bdc998 100644 --- a/src/adapter/origin.rs +++ b/src/adapter/origin.rs @@ -9,7 +9,7 @@ use crate::{ use super::{ enum_variant::{EnumVariant, LazyDiscriminants}, - vertex::{Vertex, VertexKind}, + vertex::{ImplementedTrait, Vertex, VertexKind}, }; #[non_exhaustive] @@ -78,11 +78,16 @@ impl Origin { pub(super) fn make_implemented_trait_vertex<'a>( &self, path: &'a rustdoc_types::Path, + bound: Option<&'a rustdoc_types::GenericBound>, trait_def: Option<&'a Item>, ) -> Vertex<'a> { Vertex { origin: *self, - kind: VertexKind::ImplementedTrait(path, trait_def), + kind: VertexKind::ImplementedTrait(ImplementedTrait { + path, + bound, + resolved_item: trait_def, + }), } } diff --git a/src/adapter/properties.rs b/src/adapter/properties.rs index 3370b97d..9640652b 100644 --- a/src/adapter/properties.rs +++ b/src/adapter/properties.rs @@ -491,14 +491,14 @@ pub(super) fn resolve_implemented_trait_property<'a, V: AsVertex> + ' previous_crate.as_ref().expect("no previous crate provided") } }; - let (info, item) = vertex + let impld = vertex .as_implemented_trait() .expect("not an ImplementedTrait"); - if let Some(item) = item { + if let Some(item) = impld.resolved_item { // We have the full item already. Use the original declaration name. item.name.clone().into() - } else if let Some(summary) = origin_crate.inner.paths.get(&info.id) { + } else if let Some(summary) = origin_crate.inner.paths.get(&impld.path.id) { // The item is from a foreign crate. // The last component of the canonical path should match its declaration name, // so use that. @@ -506,11 +506,11 @@ pub(super) fn resolve_implemented_trait_property<'a, V: AsVertex> + ' .path .last() .unwrap_or_else(|| { - panic!("empty path for id {} in vertex {vertex:?}", info.id.0) + panic!("empty path for id {} in vertex {vertex:?}", impld.path.id.0) }) .clone() .into() - } else if let Some((_, last)) = info.name.rsplit_once("::") { + } else if let Some((_, last)) = impld.path.name.rsplit_once("::") { // For some reason, we didn't find the item either locally or // in the `paths` section of the rustdoc JSON. // @@ -521,22 +521,22 @@ pub(super) fn resolve_implemented_trait_property<'a, V: AsVertex> + ' // Otherwise, fall through to the `else` block to return it as-is. last.to_string().into() } else { - info.name.clone().into() + impld.path.name.clone().into() } }), "instantiated_name" => resolve_property_with(contexts, |vertex| { - let (info, _) = vertex + let impld = vertex .as_implemented_trait() .expect("not an ImplementedTrait"); - super::rust_type_name::rust_type_name_from_path(info).into() + super::rust_type_name::implemented_trait_instantiated_name(impld).into() }), "trait_id" => resolve_property_with(contexts, |vertex| { - let (info, _) = vertex + let impld = vertex .as_implemented_trait() .expect("not an ImplementedTrait"); - info.id.0.to_string().into() + impld.path.id.0.to_string().into() }), _ => unreachable!("ImplementedTrait property {property_name}"), } diff --git a/src/adapter/rust_type_name.rs b/src/adapter/rust_type_name.rs index 7adac6cb..f928e52b 100644 --- a/src/adapter/rust_type_name.rs +++ b/src/adapter/rust_type_name.rs @@ -1,24 +1,34 @@ use std::fmt::{Display, Formatter, Result}; -/// Serializes the `rustdoc_types::Type` type as a String containing the name -/// of the type, as close as possible to the original code declaration. +use super::vertex::ImplementedTrait; + +/// Serializes the `rustdoc_types::Type` type as a `String` containing the name +/// of the type together with all generic parameters, +/// as close as possible to its original code declaration. pub(crate) fn rust_type_name(ty: &rustdoc_types::Type) -> String { Type(ty, false).to_string() } -pub(crate) fn rust_type_name_from_path(path: &rustdoc_types::Path) -> String { - assert!( - // No type should have code akin to `impl Fn() -> i64 for Type`, - // or else this code has a fundamental misunderstanding of what Rust can do. - !matches!( - path.args.as_deref(), - Some(rustdoc_types::GenericArgs::Parenthesized { .. }) - ), - "printing parenthesized args for a type or trait, which should be impossible: {path:?}" - ); +/// Serializes our `ImplementedTrait` type as a `String` together with all generic parameters +/// and higher-rank trait bounds (HRTBs), as close as possible to its original code declaration. +pub(crate) fn implemented_trait_instantiated_name( + implemented_trait: &ImplementedTrait<'_>, +) -> String { + if let Some(bound) = implemented_trait.bound { + generic_bound_instantiated_name(bound) + } else { + rustdoc_path_instantiated_name(implemented_trait.path) + } +} + +fn rustdoc_path_instantiated_name(path: &rustdoc_types::Path) -> String { Path(path, false).to_string() } +fn generic_bound_instantiated_name(bound: &rustdoc_types::GenericBound) -> String { + GenericBound(bound, false).to_string() +} + /// Creates a struct named `$t` that wraps a `rustdoc_types::$t` reference, /// and implements `Display` on it by calling the given `$formatter` function. macro_rules! display_wrapper { diff --git a/src/adapter/tests.rs b/src/adapter/tests.rs index 3b2c5895..0d519ecf 100644 --- a/src/adapter/tests.rs +++ b/src/adapter/tests.rs @@ -3306,3 +3306,130 @@ fn implemented_trait_instantiated_name() { similar_asserts::assert_eq!(expected_results, results); } + +#[test] +fn parenthesized_type_bounds_on_type_and_impl() { + let path = "./localdata/test_data/rust_type_name/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)); + + let query = r#" +{ + Crate { + item { + ... on Struct { + name @filter(op: "=", value: ["$struct"]) @output + + generic_parameter { + ... on GenericTypeParameter { + generic: name @output + bound_: type_bound { + instantiated_name @output + } + } + } + } + } + } +} +"#; + + let mut variables: BTreeMap<&str, &str> = BTreeMap::default(); + variables.insert("struct", "ParenthesizedGenericType"); + + 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, + generic: String, + bound_instantiated_name: String, + } + + 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: "ParenthesizedGenericType".into(), + generic: "T".into(), + bound_instantiated_name: "for<'a> Fn(&'a i64) -> &'a i64".into(), + }]; + expected_results.sort_unstable(); + + similar_asserts::assert_eq!(expected_results, results); + + let query = r#" +{ + Crate { + item { + ... on Struct { + name @filter(op: "=", value: ["$struct"]) @output + + generic_parameter { + ... on GenericTypeParameter { + generic: name @output + } + } + + impl_: inherent_impl { + generic_parameter { + ... on GenericTypeParameter { + generic: name @output + bound_: type_bound { + instantiated_name @output + } + } + } + } + } + } + } +} +"#; + + let mut variables: BTreeMap<&str, &str> = BTreeMap::default(); + variables.insert("struct", "ParenthesizedGenericImpl"); + + #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize)] + struct LatterOutput { + name: String, + generic: String, + impl_generic: String, + impl_bound_instantiated_name: String, + } + + 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![LatterOutput { + name: "ParenthesizedGenericImpl".into(), + generic: "T".into(), + impl_generic: "T".into(), + impl_bound_instantiated_name: "for<'a> Fn(&'a i64) -> &'a i64".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 18f0d3dd..8c1cdb4c 100644 --- a/src/adapter/vertex.rs +++ b/src/adapter/vertex.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, rc::Rc}; use rustdoc_types::{ - Abi, Constant, Crate, Enum, Function, GenericParamDef, Impl, Item, Module, Path, Span, Static, - Struct, Trait, Type, Union, VariantKind, + Abi, Constant, Crate, Enum, Function, GenericBound, GenericParamDef, Impl, Item, Module, Path, + Span, Static, Struct, Trait, Type, Union, VariantKind, }; use trustfall::provider::Typename; @@ -33,7 +33,7 @@ pub enum VertexKind<'a> { RawType(&'a Type), Attribute(Attribute<'a>), AttributeMetaItem(Rc>), - ImplementedTrait(&'a Path, Option<&'a Item>), // the second item is `None` if not in our crate + ImplementedTrait(ImplementedTrait<'a>), FunctionParameter(&'a str), FunctionAbi(&'a Abi), Discriminant(Cow<'a, str>), @@ -278,11 +278,9 @@ impl<'a> Vertex<'a> { } } - pub(super) fn as_implemented_trait( - &self, - ) -> Option<(&'a rustdoc_types::Path, Option<&'a Item>)> { + pub(super) fn as_implemented_trait(&self) -> Option<&ImplementedTrait<'a>> { match &self.kind { - VertexKind::ImplementedTrait(path, trait_item) => Some((*path, *trait_item)), + VertexKind::ImplementedTrait(impld) => Some(impld), _ => None, } } @@ -317,6 +315,7 @@ impl<'a> Vertex<'a> { rustdoc_types::ItemEnum::Function(x) => Some(&x.generics), rustdoc_types::ItemEnum::Trait(x) => Some(&x.generics), rustdoc_types::ItemEnum::Union(x) => Some(&x.generics), + rustdoc_types::ItemEnum::Impl(x) => Some(&x.generics), _ => None, }) } @@ -345,3 +344,16 @@ impl<'a> From<&'a Abi> for VertexKind<'a> { Self::FunctionAbi(a) } } + +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct ImplementedTrait<'a> { + /// The rustdoc `Path` item that contains the + pub(crate) path: &'a Path, + + /// Keep higher-rank trait bound (HRTBs) information, if any. + pub(crate) bound: Option<&'a GenericBound>, + + /// `None` if not in our crate + pub(crate) resolved_item: Option<&'a Item>, +} diff --git a/src/rustdoc_schema.graphql b/src/rustdoc_schema.graphql index b4bc21cc..150b7ae5 100644 --- a/src/rustdoc_schema.graphql +++ b/src/rustdoc_schema.graphql @@ -948,7 +948,7 @@ 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/latest/rustdoc_types/struct.Impl.html """ -type Impl implements Item { +type Impl implements Item & GenericItem { # properties from Item id: String! crate_id: Int! @@ -999,8 +999,13 @@ type Impl implements Item { span: Span attribute: [Attribute!] - # own edges + # edges from GenericItem + """ + Generic type, lifetime, or const parameters by which this impl is parameterized. + """ + generic_parameter: [GenericParameter] + # own edges """ The trait being implemented. Inherent impls don't have a trait. """ diff --git a/test_crates/rust_type_name/src/lib.rs b/test_crates/rust_type_name/src/lib.rs index a718f343..325daa70 100644 --- a/test_crates/rust_type_name/src/lib.rs +++ b/test_crates/rust_type_name/src/lib.rs @@ -31,3 +31,15 @@ pub struct Struct<'a, T> { } const unsafe fn x() {} + +pub struct ParenthesizedGenericType Fn(&'a i64) -> &'a i64> { + value: T, +} + +pub struct ParenthesizedGenericImpl { + value: T, +} + +impl Fn(&'a i64) -> &'a i64> ParenthesizedGenericImpl { + const N: usize = 42; +}