Skip to content

Commit

Permalink
Allow Impl blocks to have their own generic params and bounds. (#560)
Browse files Browse the repository at this point in the history
For example:
```rust
struct Example<T> {
    value: T,
}

impl<T: Debug> Example<T> {
    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.
  • Loading branch information
obi1kenobi committed Nov 7, 2024
1 parent d0fcbc0 commit 78b6c46
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 38 deletions.
21 changes: 16 additions & 5 deletions src/adapter/edges.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ pub(super) fn resolve_impl_edge<'a, V: AsVertex<Vertex<'a>> + '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())
Expand Down Expand Up @@ -599,7 +599,13 @@ pub(super) fn resolve_trait_edge<'a, V: AsVertex<Vertex<'a>> + '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
}
Expand Down Expand Up @@ -693,12 +699,13 @@ pub(super) fn resolve_implemented_trait_edge<'a, V: AsVertex<Vertex<'a>> + '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)),
)
Expand Down Expand Up @@ -861,7 +868,11 @@ pub(super) fn resolve_generic_type_parameter_edge<'a, V: AsVertex<Vertex<'a>> +
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
}
Expand Down
1 change: 1 addition & 0 deletions src/adapter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions src/adapter/origin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{

use super::{
enum_variant::{EnumVariant, LazyDiscriminants},
vertex::{Vertex, VertexKind},
vertex::{ImplementedTrait, Vertex, VertexKind},
};

#[non_exhaustive]
Expand Down Expand Up @@ -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,
}),
}
}

Expand Down
20 changes: 10 additions & 10 deletions src/adapter/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,26 +491,26 @@ pub(super) fn resolve_implemented_trait_property<'a, V: AsVertex<Vertex<'a>> + '
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.
summary
.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.
//
Expand All @@ -521,22 +521,22 @@ pub(super) fn resolve_implemented_trait_property<'a, V: AsVertex<Vertex<'a>> + '
// 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}"),
}
Expand Down
34 changes: 22 additions & 12 deletions src/adapter/rust_type_name.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
127 changes: 127 additions & 0 deletions src/adapter/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Output> =
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<LatterOutput> =
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);
}
26 changes: 19 additions & 7 deletions src/adapter/vertex.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -33,7 +33,7 @@ pub enum VertexKind<'a> {
RawType(&'a Type),
Attribute(Attribute<'a>),
AttributeMetaItem(Rc<AttributeMetaItem<'a>>),
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>),
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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,
})
}
Expand Down Expand Up @@ -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>,
}
Loading

0 comments on commit 78b6c46

Please sign in to comment.