From e13593bec8381186994ed9e2a25de0da0460a32c Mon Sep 17 00:00:00 2001 From: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> Date: Tue, 17 Mar 2026 07:33:25 +0100 Subject: [PATCH] feat: Add basic support for generic parameters set via trait-provided associated items --- compiler/ui_tests/Cargo.toml | 6 + .../bitflags_are_supported/Cargo.toml | 18 + .../bitflags_are_supported/diagnostics.dot | 41 ++ .../expectations/app.rs | 172 ++++++++ .../expectations/diagnostics.dot | 37 ++ .../generated_app/Cargo.toml | 18 + .../generated_app/src/Cargo.toml | 8 + .../generated_app/src/lib.rs | 172 ++++++++ .../bitflags_are_supported/src/lib.rs | 27 ++ .../bitflags_are_supported/src/main.rs | 24 ++ .../bitflags_are_supported/test_config.toml | 7 + .../Cargo.toml | 21 + .../diagnostics.dot | 0 .../ephemeral_deps/dep/Cargo.toml | 17 + .../ephemeral_deps/dep/src/lib.rs | 9 + .../expectations/stderr.txt | 36 ++ .../generated_app/Cargo.toml | 8 + .../generated_app/src/Cargo.toml | 8 + .../generated_app/src/lib.rs | 0 .../src/lib.rs | 21 + .../src/main.rs | 24 ++ .../test_config.toml | 7 + .../Cargo.toml | 21 + .../diagnostics.dot | 41 ++ .../ephemeral_deps/dep/Cargo.toml | 17 + .../ephemeral_deps/dep/src/lib.rs | 11 + .../expectations/app.rs | 172 ++++++++ .../expectations/diagnostics.dot | 37 ++ .../generated_app/Cargo.toml | 18 + .../generated_app/src/Cargo.toml | 8 + .../generated_app/src/lib.rs | 172 ++++++++ .../src/lib.rs | 19 + .../src/main.rs | 24 ++ .../test_config.toml | 7 + rustdoc/rustdoc_resolver/src/errors.rs | 34 ++ rustdoc/rustdoc_resolver/src/lib.rs | 2 +- rustdoc/rustdoc_resolver/src/resolve_type.rs | 369 +++++++++++++++++- 37 files changed, 1611 insertions(+), 22 deletions(-) create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/Cargo.toml create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/diagnostics.dot create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/expectations/app.rs create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/expectations/diagnostics.dot create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/generated_app/Cargo.toml create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/Cargo.toml create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/lib.rs create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/src/lib.rs create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/src/main.rs create mode 100644 compiler/ui_tests/reflection/bitflags_are_supported/test_config.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/diagnostics.dot create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/src/lib.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/expectations/stderr.txt create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/src/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/src/lib.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/lib.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/main.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/test_config.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/diagnostics.dot create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/src/lib.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/app.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/diagnostics.dot create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/Cargo.toml create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/lib.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/src/lib.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/src/main.rs create mode 100644 compiler/ui_tests/reflection/qualified_path_default_with_struct/test_config.toml diff --git a/compiler/ui_tests/Cargo.toml b/compiler/ui_tests/Cargo.toml index 0498655a8..917dcfe54 100644 --- a/compiler/ui_tests/Cargo.toml +++ b/compiler/ui_tests/Cargo.toml @@ -291,6 +291,8 @@ members = [ "reflection/arc_singletons_are_supported/generated_app", "reflection/arrays_are_supported", "reflection/arrays_are_supported/generated_app", + "reflection/bitflags_are_supported", + "reflection/bitflags_are_supported/generated_app", "reflection/common_response_types_are_supported", "reflection/common_response_types_are_supported/generated_app", "reflection/crate_resolution/dependencies_can_register_local_items", @@ -319,6 +321,10 @@ members = [ "reflection/output_parameter_cannot_be_handled/generated_app", "reflection/pattern_bindings_in_input_parameters_are_supported", "reflection/pattern_bindings_in_input_parameters_are_supported/generated_app", + "reflection/qualified_path_default_with_blanket_impl", + "reflection/qualified_path_default_with_blanket_impl/generated_app", + "reflection/qualified_path_default_with_struct", + "reflection/qualified_path_default_with_struct/generated_app", "reflection/raw_pointers_are_supported", "reflection/raw_pointers_are_supported/generated_app", "reflection/reexported_type_alias_work", diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/Cargo.toml b/compiler/ui_tests/reflection/bitflags_are_supported/Cargo.toml new file mode 100644 index 000000000..7194e27a9 --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "app_ad701040" +version = "0.1.0" +edition.workspace = true + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = ["cfg(pavex_ide_hint)"] + +[dependencies] +enumflags2 = "0.7" +workspace_hack = { version = "0.1", path = "../../workspace_hack" } + +[dependencies.pavex] +workspace = true + +[dependencies.pavex_cli_client] +workspace = true diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/diagnostics.dot b/compiler/ui_tests/reflection/bitflags_are_supported/diagnostics.dot new file mode 100644 index 000000000..3e7c03963 --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/diagnostics.dot @@ -0,0 +1,41 @@ +digraph "* * - 0" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| crate::route_0::Next0(&'a pavex::router::AllowedMethods) -> crate::route_0::Next0<'a>"] + 2 [ label = "2| pavex::middleware::Next::new(crate::route_0::Next0<'a>) -> pavex::middleware::Next>"] + 3 [ label = "3| pavex::middleware::wrap_noop(pavex::middleware::Next>) -> pavex::Response"] + 4 [ label = "4| ::into_response(pavex::Response) -> pavex::Response"] + 2 -> 3 [ ] + 1 -> 2 [ ] + 3 -> 4 [ ] + 0 -> 1 [ ] +} + +digraph "* * - 1" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| pavex::router::default_fallback(&pavex::router::AllowedMethods) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] +} + +digraph "GET / - 0" { + 0 [ label = "0| crate::route_1::Next0() -> crate::route_1::Next0"] + 1 [ label = "1| pavex::middleware::Next::new(crate::route_1::Next0) -> pavex::middleware::Next"] + 2 [ label = "2| pavex::middleware::wrap_noop(pavex::middleware::Next) -> pavex::Response"] + 3 [ label = "3| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] + 2 -> 3 [ ] +} + +digraph "GET / - 1" { + 0 [ label = "0| app_ad701040::flags() -> enumflags2::BitFlags"] + 1 [ label = "1| app_ad701040::handler(enumflags2::BitFlags) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 0 -> 1 [ ] + 1 -> 2 [ ] +} + +digraph app_state { + 0 [ label = "0| crate::ApplicationState() -> crate::ApplicationState"] +} diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/expectations/app.rs b/compiler/ui_tests/reflection/bitflags_are_supported/expectations/app.rs new file mode 100644 index 000000000..6a6eddc92 --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/expectations/app.rs @@ -0,0 +1,172 @@ +//! Do NOT edit this code. +//! It was automatically generated by Pavex. +//! All manual edits will be lost next time the code is generated. +extern crate alloc; +struct ServerState { + router: Router, + #[allow(dead_code)] + application_state: ApplicationState, +} +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ApplicationConfig {} +pub struct ApplicationState {} +impl ApplicationState { + pub async fn new( + _app_config: crate::ApplicationConfig, + ) -> Result { + Ok(Self::_new().await) + } + async fn _new() -> crate::ApplicationState { + crate::ApplicationState {} + } +} +#[derive(Debug, thiserror::Error)] +pub enum ApplicationStateError {} +pub fn run( + server_builder: pavex::server::Server, + application_state: ApplicationState, +) -> pavex::server::ServerHandle { + async fn handler( + request: http::Request, + connection_info: Option, + server_state: std::sync::Arc, + ) -> pavex::Response { + let (router, state) = (&server_state.router, &server_state.application_state); + router.route(request, connection_info, state).await + } + let router = Router::new(); + let server_state = std::sync::Arc::new(ServerState { + router, + application_state, + }); + server_builder.serve(handler, server_state) +} +struct Router { + router: matchit::Router, +} +impl Router { + /// Create a new router instance. + /// + /// This method is invoked once, when the server starts. + pub fn new() -> Self { + Self { router: Self::router() } + } + fn router() -> matchit::Router { + let mut router = matchit::Router::new(); + router.insert("/", 0u32).unwrap(); + router + } + pub async fn route( + &self, + request: http::Request, + _connection_info: Option, + #[allow(unused)] + state: &ApplicationState, + ) -> pavex::Response { + let (request_head, _) = request.into_parts(); + let request_head: pavex::request::RequestHead = request_head.into(); + let Ok(matched_route) = self.router.at(&request_head.target.path()) else { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter( + vec![], + ) + .into(); + return route_0::entrypoint(&allowed_methods).await; + }; + match matched_route.value { + 0u32 => { + match &request_head.method { + &pavex::http::Method::GET => route_1::entrypoint().await, + _ => { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ + pavex::http::Method::GET, + ]) + .into(); + route_0::entrypoint(&allowed_methods).await + } + } + } + i => unreachable!("Unknown route id: {}", i), + } + } +} +pub mod route_0 { + pub async fn entrypoint<'a>( + s_0: &'a pavex::router::AllowedMethods, + ) -> pavex::Response { + let response = wrapping_0(s_0).await; + response + } + async fn stage_1<'a>(s_0: &'a pavex::router::AllowedMethods) -> pavex::Response { + let response = handler(s_0).await; + response + } + async fn wrapping_0(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = crate::route_0::Next0 { + s_0: v0, + next: stage_1, + }; + let v2 = pavex::middleware::Next::new(v1); + let v3 = pavex::middleware::wrap_noop(v2).await; + ::into_response(v3) + } + async fn handler(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = pavex::router::default_fallback(v0).await; + ::into_response(v1) + } + struct Next0<'a, T> + where + T: std::future::Future, + { + s_0: &'a pavex::router::AllowedMethods, + next: fn(&'a pavex::router::AllowedMethods) -> T, + } + impl<'a, T> std::future::IntoFuture for Next0<'a, T> + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)(self.s_0) + } + } +} +pub mod route_1 { + pub async fn entrypoint() -> pavex::Response { + let response = wrapping_0().await; + response + } + async fn stage_1() -> pavex::Response { + let response = handler().await; + response + } + async fn wrapping_0() -> pavex::Response { + let v0 = crate::route_1::Next0 { + next: stage_1, + }; + let v1 = pavex::middleware::Next::new(v0); + let v2 = pavex::middleware::wrap_noop(v1).await; + ::into_response(v2) + } + async fn handler() -> pavex::Response { + let v0 = app::flags(); + let v1 = app::handler(v0); + ::into_response(v1) + } + struct Next0 + where + T: std::future::Future, + { + next: fn() -> T, + } + impl std::future::IntoFuture for Next0 + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)() + } + } +} \ No newline at end of file diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/expectations/diagnostics.dot b/compiler/ui_tests/reflection/bitflags_are_supported/expectations/diagnostics.dot new file mode 100644 index 000000000..63cbe498a --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/expectations/diagnostics.dot @@ -0,0 +1,37 @@ +digraph "* * - 0" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| crate::route_0::Next0(&'a pavex::router::AllowedMethods) -> crate::route_0::Next0<'a>"] + 2 [ label = "2| pavex::middleware::Next::new(crate::route_0::Next0<'a>) -> pavex::middleware::Next>"] + 3 [ label = "3| pavex::middleware::wrap_noop(pavex::middleware::Next>) -> pavex::Response"] + 4 [ label = "4| ::into_response(pavex::Response) -> pavex::Response"] + 2 -> 3 [ ] + 1 -> 2 [ ] + 3 -> 4 [ ] + 0 -> 1 [ ] +} +digraph "* * - 1" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| pavex::router::default_fallback(&pavex::router::AllowedMethods) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] +} +digraph "GET / - 0" { + 0 [ label = "0| crate::route_1::Next0() -> crate::route_1::Next0"] + 1 [ label = "1| pavex::middleware::Next::new(crate::route_1::Next0) -> pavex::middleware::Next"] + 2 [ label = "2| pavex::middleware::wrap_noop(pavex::middleware::Next) -> pavex::Response"] + 3 [ label = "3| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] + 2 -> 3 [ ] +} +digraph "GET / - 1" { + 0 [ label = "0| app::flags() -> enumflags2::BitFlags"] + 1 [ label = "1| app::handler(enumflags2::BitFlags) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 0 -> 1 [ ] + 1 -> 2 [ ] +} +digraph app_state { + 0 [ label = "0| crate::ApplicationState() -> crate::ApplicationState"] +} \ No newline at end of file diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/Cargo.toml b/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/Cargo.toml new file mode 100644 index 000000000..768382182 --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "application_ad701040" +version = "0.1.0" +edition = "2024" + +[package.metadata.px.generate] +generator_type = "cargo_workspace_binary" +generator_name = "app_ad701040" + +[dependencies] +app_ad701040 = { version = "0.1", path = "..", default-features = false } +enumflags2 = { version = "0.7", default-features = false } +http = { version = "1", default-features = false } +hyper = { version = "1", default-features = false } +matchit = { version = "0.9", default-features = false } +pavex = { version = "0.2", path = "../../../../../runtime/pavex", default-features = false } +serde = { version = "1", default-features = false } +thiserror = { version = "2", default-features = false } diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/Cargo.toml b/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/Cargo.toml new file mode 100644 index 000000000..abb8f0b07 --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "application_ad701040" +version = "0.1.0" +edition = "2021" + +[package.metadata.px.generate] +generator_type = "cargo_workspace_binary" +generator_name = "app_ad701040" diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/lib.rs b/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/lib.rs new file mode 100644 index 000000000..102917b81 --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/generated_app/src/lib.rs @@ -0,0 +1,172 @@ +//! Do NOT edit this code. +//! It was automatically generated by Pavex. +//! All manual edits will be lost next time the code is generated. +extern crate alloc; +struct ServerState { + router: Router, + #[allow(dead_code)] + application_state: ApplicationState, +} +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ApplicationConfig {} +pub struct ApplicationState {} +impl ApplicationState { + pub async fn new( + _app_config: crate::ApplicationConfig, + ) -> Result { + Ok(Self::_new().await) + } + async fn _new() -> crate::ApplicationState { + crate::ApplicationState {} + } +} +#[derive(Debug, thiserror::Error)] +pub enum ApplicationStateError {} +pub fn run( + server_builder: pavex::server::Server, + application_state: ApplicationState, +) -> pavex::server::ServerHandle { + async fn handler( + request: http::Request, + connection_info: Option, + server_state: std::sync::Arc, + ) -> pavex::Response { + let (router, state) = (&server_state.router, &server_state.application_state); + router.route(request, connection_info, state).await + } + let router = Router::new(); + let server_state = std::sync::Arc::new(ServerState { + router, + application_state, + }); + server_builder.serve(handler, server_state) +} +struct Router { + router: matchit::Router, +} +impl Router { + /// Create a new router instance. + /// + /// This method is invoked once, when the server starts. + pub fn new() -> Self { + Self { router: Self::router() } + } + fn router() -> matchit::Router { + let mut router = matchit::Router::new(); + router.insert("/", 0u32).unwrap(); + router + } + pub async fn route( + &self, + request: http::Request, + _connection_info: Option, + #[allow(unused)] + state: &ApplicationState, + ) -> pavex::Response { + let (request_head, _) = request.into_parts(); + let request_head: pavex::request::RequestHead = request_head.into(); + let Ok(matched_route) = self.router.at(&request_head.target.path()) else { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter( + vec![], + ) + .into(); + return route_0::entrypoint(&allowed_methods).await; + }; + match matched_route.value { + 0u32 => { + match &request_head.method { + &pavex::http::Method::GET => route_1::entrypoint().await, + _ => { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ + pavex::http::Method::GET, + ]) + .into(); + route_0::entrypoint(&allowed_methods).await + } + } + } + i => unreachable!("Unknown route id: {}", i), + } + } +} +pub mod route_0 { + pub async fn entrypoint<'a>( + s_0: &'a pavex::router::AllowedMethods, + ) -> pavex::Response { + let response = wrapping_0(s_0).await; + response + } + async fn stage_1<'a>(s_0: &'a pavex::router::AllowedMethods) -> pavex::Response { + let response = handler(s_0).await; + response + } + async fn wrapping_0(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = crate::route_0::Next0 { + s_0: v0, + next: stage_1, + }; + let v2 = pavex::middleware::Next::new(v1); + let v3 = pavex::middleware::wrap_noop(v2).await; + ::into_response(v3) + } + async fn handler(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = pavex::router::default_fallback(v0).await; + ::into_response(v1) + } + struct Next0<'a, T> + where + T: std::future::Future, + { + s_0: &'a pavex::router::AllowedMethods, + next: fn(&'a pavex::router::AllowedMethods) -> T, + } + impl<'a, T> std::future::IntoFuture for Next0<'a, T> + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)(self.s_0) + } + } +} +pub mod route_1 { + pub async fn entrypoint() -> pavex::Response { + let response = wrapping_0().await; + response + } + async fn stage_1() -> pavex::Response { + let response = handler().await; + response + } + async fn wrapping_0() -> pavex::Response { + let v0 = crate::route_1::Next0 { + next: stage_1, + }; + let v1 = pavex::middleware::Next::new(v0); + let v2 = pavex::middleware::wrap_noop(v1).await; + ::into_response(v2) + } + async fn handler() -> pavex::Response { + let v0 = app_ad701040::flags(); + let v1 = app_ad701040::handler(v0); + ::into_response(v1) + } + struct Next0 + where + T: std::future::Future, + { + next: fn() -> T, + } + impl std::future::IntoFuture for Next0 + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)() + } + } +} diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/src/lib.rs b/compiler/ui_tests/reflection/bitflags_are_supported/src/lib.rs new file mode 100644 index 000000000..3a97b6aba --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/src/lib.rs @@ -0,0 +1,27 @@ +use enumflags2::{bitflags, BitFlags}; +use pavex::{blueprint::from, Blueprint}; + +#[bitflags] +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum MyFlag { + A = 0b0001, + B = 0b0010, +} + +#[pavex::request_scoped] +pub fn flags() -> BitFlags { + BitFlags::empty() +} + +#[pavex::get(path = "/")] +pub fn handler(_flags: BitFlags) -> pavex::Response { + todo!() +} + +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + bp.import(from![crate]); + bp.routes(from![crate]); + bp +} diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/src/main.rs b/compiler/ui_tests/reflection/bitflags_are_supported/src/main.rs new file mode 100644 index 000000000..35aa4ffaf --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/src/main.rs @@ -0,0 +1,24 @@ +//! This code is generated by `pavex_test_runner`, +//! Do NOT modify it manually. +use app_ad701040::blueprint; +use pavex_cli_client::{Client, config::Color}; +use pavex_cli_client::commands::generate::GenerateError; + +fn main() -> Result<(), Box> { + let ui_test_dir: std::path::PathBuf = std::env::var("UI_TEST_DIR").unwrap().into(); + let outcome = Client::new() + .color(Color::Always) + .pavex_cli_path(std::env::var("PAVEX_TEST_CLI_PATH").unwrap().into()) + .generate(blueprint(), ui_test_dir.join("generated_app")) + .diagnostics_path("diagnostics.dot".into()) + .execute(); + match outcome { + Ok(_) => {}, + Err(GenerateError::NonZeroExitCode(_)) => { std::process::exit(1); } + Err(e) => { + eprintln!("Failed to invoke `pavex generate`.\n{:?}", e); + std::process::exit(1); + } + } + Ok(()) +} diff --git a/compiler/ui_tests/reflection/bitflags_are_supported/test_config.toml b/compiler/ui_tests/reflection/bitflags_are_supported/test_config.toml new file mode 100644 index 000000000..89192959e --- /dev/null +++ b/compiler/ui_tests/reflection/bitflags_are_supported/test_config.toml @@ -0,0 +1,7 @@ +description = "pavex supports enumflags2::BitFlags as input parameters" + +[expectations] +codegen = "pass" + +[dependencies] +enumflags2 = "0.7" diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/Cargo.toml new file mode 100644 index 000000000..cc0f6bdd6 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "app_d090a519" +version = "0.1.0" +edition.workspace = true + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = ["cfg(pavex_ide_hint)"] + +[dependencies] +workspace_hack = { version = "0.1", path = "../../workspace_hack" } + +[dependencies.pavex] +workspace = true + +[dependencies.pavex_cli_client] +workspace = true + +[dependencies.dep] +path = "ephemeral_deps/dep" +package = "dep_d090a519" diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/diagnostics.dot b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/diagnostics.dot new file mode 100644 index 000000000..e69de29bb diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/Cargo.toml new file mode 100644 index 000000000..db60b6044 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dep_d090a519" +version = "0.1.0" +edition.workspace = true + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = ["cfg(pavex_ide_hint)"] + +[lints.rust.unused] +level = "allow" + +[dependencies] +workspace_hack = { version = "0.1", path = "../../../../workspace_hack" } + +[dependencies.pavex] +workspace = true diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/src/lib.rs b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/src/lib.rs new file mode 100644 index 000000000..b8e67b405 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/ephemeral_deps/dep/src/lib.rs @@ -0,0 +1,9 @@ +pub trait HasDefault { + type Default; +} + +impl HasDefault for T { + type Default = u8; +} + +pub struct Container::Default>(pub std::marker::PhantomData<(T, D)>); diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/expectations/stderr.txt b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/expectations/stderr.txt new file mode 100644 index 000000000..3c58e8a68 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/expectations/stderr.txt @@ -0,0 +1,36 @@ +ERROR: + Γ— I don't know how to handle the type returned by + β”‚ `app::get_container`. + β”‚ + β”‚ ╭─[reflection/qualified_path_default_with_blanket_impl/src/lib.rs:5:1] + β”‚ 5 β”‚ + β”‚ 6 β”‚ β•­─β–Ά #[pavex::request_scoped] + β”‚ 7 β”‚ β”œ─β–Ά pub fn get_container() -> Container { + β”‚ Β· β•°──── The constructor was registered here + β”‚ 8 β”‚ Container(std::marker::PhantomData) + β”‚ ╰──── + β”‚ ╭─[reflection/qualified_path_default_with_blanket_impl/src/lib.rs:6:1] + β”‚ 6 β”‚ #[pavex::request_scoped] + β”‚ 7 β”‚ pub fn get_container() -> Container { + β”‚ Β·  ────────┬──────── + β”‚ Β· ╰── The output type that I can't handle + β”‚ 8 β”‚ Container(std::marker::PhantomData) + β”‚ ╰──── +ERROR: + Γ— One of the input parameters for `app::handler` has a type that I + β”‚ can't handle. + β”‚ + β”‚ ╭─[reflection/qualified_path_default_with_blanket_impl/src/lib.rs:10:1] + β”‚ 10 β”‚ + β”‚ 11 β”‚ β•­─β–Ά #[pavex::get(path = "/")] + β”‚ 12 β”‚ β”œ─β–Ά pub fn handler(_c: Container) -> pavex::Response { + β”‚ Β· β•°──── The request handler was registered here + β”‚ 13 β”‚ todo!() + β”‚ ╰──── + β”‚ ╭─[reflection/qualified_path_default_with_blanket_impl/src/lib.rs:11:1] + β”‚ 11 β”‚ #[pavex::get(path = "/")] + β”‚ 12 β”‚ pub fn handler(_c: Container) -> pavex::Response { + β”‚ Β·  ──────────┬────────── + β”‚ Β· ╰── I don't know how handle this parameter + β”‚ 13 β”‚ todo!() + β”‚ ╰──── \ No newline at end of file diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/Cargo.toml new file mode 100644 index 000000000..451c39c27 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "application_d090a519" +version = "0.1.0" +edition = "2024" + +[package.metadata.px.generate] +generator_type = "cargo_workspace_binary" +generator_name = "app_d090a519" diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/src/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/src/Cargo.toml new file mode 100644 index 000000000..1c85ab9d7 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/src/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "application_d090a519" +version = "0.1.0" +edition = "2021" + +[package.metadata.px.generate] +generator_type = "cargo_workspace_binary" +generator_name = "app_d090a519" diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/src/lib.rs b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/generated_app/src/lib.rs new file mode 100644 index 000000000..e69de29bb diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/lib.rs b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/lib.rs new file mode 100644 index 000000000..1b0c8c068 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/lib.rs @@ -0,0 +1,21 @@ +use dep::Container; +use pavex::{blueprint::from, Blueprint}; + +pub struct MyType; + +#[pavex::request_scoped] +pub fn get_container() -> Container { + Container(std::marker::PhantomData) +} + +#[pavex::get(path = "/")] +pub fn handler(_c: Container) -> pavex::Response { + todo!() +} + +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + bp.import(from![crate]); + bp.routes(from![crate]); + bp +} diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/main.rs b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/main.rs new file mode 100644 index 000000000..1be0be549 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/src/main.rs @@ -0,0 +1,24 @@ +//! This code is generated by `pavex_test_runner`, +//! Do NOT modify it manually. +use app_d090a519::blueprint; +use pavex_cli_client::{Client, config::Color}; +use pavex_cli_client::commands::generate::GenerateError; + +fn main() -> Result<(), Box> { + let ui_test_dir: std::path::PathBuf = std::env::var("UI_TEST_DIR").unwrap().into(); + let outcome = Client::new() + .color(Color::Always) + .pavex_cli_path(std::env::var("PAVEX_TEST_CLI_PATH").unwrap().into()) + .generate(blueprint(), ui_test_dir.join("generated_app")) + .diagnostics_path("diagnostics.dot".into()) + .execute(); + match outcome { + Ok(_) => {}, + Err(GenerateError::NonZeroExitCode(_)) => { std::process::exit(1); } + Err(e) => { + eprintln!("Failed to invoke `pavex generate`.\n{:?}", e); + std::process::exit(1); + } + } + Ok(()) +} diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/test_config.toml b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/test_config.toml new file mode 100644 index 000000000..9691b3ace --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_blanket_impl/test_config.toml @@ -0,0 +1,7 @@ +description = "pavex returns a clear error when a qualified path default relies on a blanket impl" + +[expectations] +codegen = "fail" + +[ephemeral_dependencies] +dep = { path = "dep.rs" } diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_struct/Cargo.toml new file mode 100644 index 000000000..29bd3e9f3 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "app_dfe43047" +version = "0.1.0" +edition.workspace = true + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = ["cfg(pavex_ide_hint)"] + +[dependencies] +workspace_hack = { version = "0.1", path = "../../workspace_hack" } + +[dependencies.pavex] +workspace = true + +[dependencies.pavex_cli_client] +workspace = true + +[dependencies.dep] +path = "ephemeral_deps/dep" +package = "dep_dfe43047" diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/diagnostics.dot b/compiler/ui_tests/reflection/qualified_path_default_with_struct/diagnostics.dot new file mode 100644 index 000000000..7634ce1b4 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/diagnostics.dot @@ -0,0 +1,41 @@ +digraph "* * - 0" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| crate::route_0::Next0(&'a pavex::router::AllowedMethods) -> crate::route_0::Next0<'a>"] + 2 [ label = "2| pavex::middleware::Next::new(crate::route_0::Next0<'a>) -> pavex::middleware::Next>"] + 3 [ label = "3| pavex::middleware::wrap_noop(pavex::middleware::Next>) -> pavex::Response"] + 4 [ label = "4| ::into_response(pavex::Response) -> pavex::Response"] + 2 -> 3 [ ] + 1 -> 2 [ ] + 3 -> 4 [ ] + 0 -> 1 [ ] +} + +digraph "* * - 1" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| pavex::router::default_fallback(&pavex::router::AllowedMethods) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] +} + +digraph "GET / - 0" { + 0 [ label = "0| crate::route_1::Next0() -> crate::route_1::Next0"] + 1 [ label = "1| pavex::middleware::Next::new(crate::route_1::Next0) -> pavex::middleware::Next"] + 2 [ label = "2| pavex::middleware::wrap_noop(pavex::middleware::Next) -> pavex::Response"] + 3 [ label = "3| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] + 2 -> 3 [ ] +} + +digraph "GET / - 1" { + 0 [ label = "0| app_dfe43047::get_keyed() -> dep_dfe43047::Keyed"] + 1 [ label = "1| app_dfe43047::handler(dep_dfe43047::Keyed) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 0 -> 1 [ ] + 1 -> 2 [ ] +} + +digraph app_state { + 0 [ label = "0| crate::ApplicationState() -> crate::ApplicationState"] +} diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/Cargo.toml new file mode 100644 index 000000000..a23dc2d7a --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dep_dfe43047" +version = "0.1.0" +edition.workspace = true + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = ["cfg(pavex_ide_hint)"] + +[lints.rust.unused] +level = "allow" + +[dependencies] +workspace_hack = { version = "0.1", path = "../../../../workspace_hack" } + +[dependencies.pavex] +workspace = true diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/src/lib.rs b/compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/src/lib.rs new file mode 100644 index 000000000..1b6d9289a --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/ephemeral_deps/dep/src/lib.rs @@ -0,0 +1,11 @@ +pub trait HasId { + type Id; +} + +pub struct User; + +impl HasId for User { + type Id = u64; +} + +pub struct Keyed::Id>(pub std::marker::PhantomData<(T, K)>); diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/app.rs b/compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/app.rs new file mode 100644 index 000000000..70e1194fb --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/app.rs @@ -0,0 +1,172 @@ +//! Do NOT edit this code. +//! It was automatically generated by Pavex. +//! All manual edits will be lost next time the code is generated. +extern crate alloc; +struct ServerState { + router: Router, + #[allow(dead_code)] + application_state: ApplicationState, +} +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ApplicationConfig {} +pub struct ApplicationState {} +impl ApplicationState { + pub async fn new( + _app_config: crate::ApplicationConfig, + ) -> Result { + Ok(Self::_new().await) + } + async fn _new() -> crate::ApplicationState { + crate::ApplicationState {} + } +} +#[derive(Debug, thiserror::Error)] +pub enum ApplicationStateError {} +pub fn run( + server_builder: pavex::server::Server, + application_state: ApplicationState, +) -> pavex::server::ServerHandle { + async fn handler( + request: http::Request, + connection_info: Option, + server_state: std::sync::Arc, + ) -> pavex::Response { + let (router, state) = (&server_state.router, &server_state.application_state); + router.route(request, connection_info, state).await + } + let router = Router::new(); + let server_state = std::sync::Arc::new(ServerState { + router, + application_state, + }); + server_builder.serve(handler, server_state) +} +struct Router { + router: matchit::Router, +} +impl Router { + /// Create a new router instance. + /// + /// This method is invoked once, when the server starts. + pub fn new() -> Self { + Self { router: Self::router() } + } + fn router() -> matchit::Router { + let mut router = matchit::Router::new(); + router.insert("/", 0u32).unwrap(); + router + } + pub async fn route( + &self, + request: http::Request, + _connection_info: Option, + #[allow(unused)] + state: &ApplicationState, + ) -> pavex::Response { + let (request_head, _) = request.into_parts(); + let request_head: pavex::request::RequestHead = request_head.into(); + let Ok(matched_route) = self.router.at(&request_head.target.path()) else { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter( + vec![], + ) + .into(); + return route_0::entrypoint(&allowed_methods).await; + }; + match matched_route.value { + 0u32 => { + match &request_head.method { + &pavex::http::Method::GET => route_1::entrypoint().await, + _ => { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ + pavex::http::Method::GET, + ]) + .into(); + route_0::entrypoint(&allowed_methods).await + } + } + } + i => unreachable!("Unknown route id: {}", i), + } + } +} +pub mod route_0 { + pub async fn entrypoint<'a>( + s_0: &'a pavex::router::AllowedMethods, + ) -> pavex::Response { + let response = wrapping_0(s_0).await; + response + } + async fn stage_1<'a>(s_0: &'a pavex::router::AllowedMethods) -> pavex::Response { + let response = handler(s_0).await; + response + } + async fn wrapping_0(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = crate::route_0::Next0 { + s_0: v0, + next: stage_1, + }; + let v2 = pavex::middleware::Next::new(v1); + let v3 = pavex::middleware::wrap_noop(v2).await; + ::into_response(v3) + } + async fn handler(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = pavex::router::default_fallback(v0).await; + ::into_response(v1) + } + struct Next0<'a, T> + where + T: std::future::Future, + { + s_0: &'a pavex::router::AllowedMethods, + next: fn(&'a pavex::router::AllowedMethods) -> T, + } + impl<'a, T> std::future::IntoFuture for Next0<'a, T> + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)(self.s_0) + } + } +} +pub mod route_1 { + pub async fn entrypoint() -> pavex::Response { + let response = wrapping_0().await; + response + } + async fn stage_1() -> pavex::Response { + let response = handler().await; + response + } + async fn wrapping_0() -> pavex::Response { + let v0 = crate::route_1::Next0 { + next: stage_1, + }; + let v1 = pavex::middleware::Next::new(v0); + let v2 = pavex::middleware::wrap_noop(v1).await; + ::into_response(v2) + } + async fn handler() -> pavex::Response { + let v0 = app::get_keyed(); + let v1 = app::handler(v0); + ::into_response(v1) + } + struct Next0 + where + T: std::future::Future, + { + next: fn() -> T, + } + impl std::future::IntoFuture for Next0 + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)() + } + } +} \ No newline at end of file diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/diagnostics.dot b/compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/diagnostics.dot new file mode 100644 index 000000000..9583abf19 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/expectations/diagnostics.dot @@ -0,0 +1,37 @@ +digraph "* * - 0" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| crate::route_0::Next0(&'a pavex::router::AllowedMethods) -> crate::route_0::Next0<'a>"] + 2 [ label = "2| pavex::middleware::Next::new(crate::route_0::Next0<'a>) -> pavex::middleware::Next>"] + 3 [ label = "3| pavex::middleware::wrap_noop(pavex::middleware::Next>) -> pavex::Response"] + 4 [ label = "4| ::into_response(pavex::Response) -> pavex::Response"] + 2 -> 3 [ ] + 1 -> 2 [ ] + 3 -> 4 [ ] + 0 -> 1 [ ] +} +digraph "* * - 1" { + 0 [ label = "0| &pavex::router::AllowedMethods"] + 1 [ label = "1| pavex::router::default_fallback(&pavex::router::AllowedMethods) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] +} +digraph "GET / - 0" { + 0 [ label = "0| crate::route_1::Next0() -> crate::route_1::Next0"] + 1 [ label = "1| pavex::middleware::Next::new(crate::route_1::Next0) -> pavex::middleware::Next"] + 2 [ label = "2| pavex::middleware::wrap_noop(pavex::middleware::Next) -> pavex::Response"] + 3 [ label = "3| ::into_response(pavex::Response) -> pavex::Response"] + 1 -> 2 [ ] + 0 -> 1 [ ] + 2 -> 3 [ ] +} +digraph "GET / - 1" { + 0 [ label = "0| app::get_keyed() -> dep_dfe43047::Keyed"] + 1 [ label = "1| app::handler(dep_dfe43047::Keyed) -> pavex::Response"] + 2 [ label = "2| ::into_response(pavex::Response) -> pavex::Response"] + 0 -> 1 [ ] + 1 -> 2 [ ] +} +digraph app_state { + 0 [ label = "0| crate::ApplicationState() -> crate::ApplicationState"] +} \ No newline at end of file diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/Cargo.toml new file mode 100644 index 000000000..dfa4fde7d --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "application_dfe43047" +version = "0.1.0" +edition = "2024" + +[package.metadata.px.generate] +generator_type = "cargo_workspace_binary" +generator_name = "app_dfe43047" + +[dependencies] +app_dfe43047 = { version = "0.1", path = "..", default-features = false } +dep_dfe43047 = { version = "0.1", path = "../ephemeral_deps/dep", default-features = false } +http = { version = "1", default-features = false } +hyper = { version = "1", default-features = false } +matchit = { version = "0.9", default-features = false } +pavex = { version = "0.2", path = "../../../../../runtime/pavex", default-features = false } +serde = { version = "1", default-features = false } +thiserror = { version = "2", default-features = false } diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/Cargo.toml b/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/Cargo.toml new file mode 100644 index 000000000..b7281fe70 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "application_dfe43047" +version = "0.1.0" +edition = "2021" + +[package.metadata.px.generate] +generator_type = "cargo_workspace_binary" +generator_name = "app_dfe43047" diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/lib.rs b/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/lib.rs new file mode 100644 index 000000000..756862590 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/generated_app/src/lib.rs @@ -0,0 +1,172 @@ +//! Do NOT edit this code. +//! It was automatically generated by Pavex. +//! All manual edits will be lost next time the code is generated. +extern crate alloc; +struct ServerState { + router: Router, + #[allow(dead_code)] + application_state: ApplicationState, +} +#[derive(Debug, Clone, serde::Deserialize)] +pub struct ApplicationConfig {} +pub struct ApplicationState {} +impl ApplicationState { + pub async fn new( + _app_config: crate::ApplicationConfig, + ) -> Result { + Ok(Self::_new().await) + } + async fn _new() -> crate::ApplicationState { + crate::ApplicationState {} + } +} +#[derive(Debug, thiserror::Error)] +pub enum ApplicationStateError {} +pub fn run( + server_builder: pavex::server::Server, + application_state: ApplicationState, +) -> pavex::server::ServerHandle { + async fn handler( + request: http::Request, + connection_info: Option, + server_state: std::sync::Arc, + ) -> pavex::Response { + let (router, state) = (&server_state.router, &server_state.application_state); + router.route(request, connection_info, state).await + } + let router = Router::new(); + let server_state = std::sync::Arc::new(ServerState { + router, + application_state, + }); + server_builder.serve(handler, server_state) +} +struct Router { + router: matchit::Router, +} +impl Router { + /// Create a new router instance. + /// + /// This method is invoked once, when the server starts. + pub fn new() -> Self { + Self { router: Self::router() } + } + fn router() -> matchit::Router { + let mut router = matchit::Router::new(); + router.insert("/", 0u32).unwrap(); + router + } + pub async fn route( + &self, + request: http::Request, + _connection_info: Option, + #[allow(unused)] + state: &ApplicationState, + ) -> pavex::Response { + let (request_head, _) = request.into_parts(); + let request_head: pavex::request::RequestHead = request_head.into(); + let Ok(matched_route) = self.router.at(&request_head.target.path()) else { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter( + vec![], + ) + .into(); + return route_0::entrypoint(&allowed_methods).await; + }; + match matched_route.value { + 0u32 => { + match &request_head.method { + &pavex::http::Method::GET => route_1::entrypoint().await, + _ => { + let allowed_methods: pavex::router::AllowedMethods = pavex::router::MethodAllowList::from_iter([ + pavex::http::Method::GET, + ]) + .into(); + route_0::entrypoint(&allowed_methods).await + } + } + } + i => unreachable!("Unknown route id: {}", i), + } + } +} +pub mod route_0 { + pub async fn entrypoint<'a>( + s_0: &'a pavex::router::AllowedMethods, + ) -> pavex::Response { + let response = wrapping_0(s_0).await; + response + } + async fn stage_1<'a>(s_0: &'a pavex::router::AllowedMethods) -> pavex::Response { + let response = handler(s_0).await; + response + } + async fn wrapping_0(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = crate::route_0::Next0 { + s_0: v0, + next: stage_1, + }; + let v2 = pavex::middleware::Next::new(v1); + let v3 = pavex::middleware::wrap_noop(v2).await; + ::into_response(v3) + } + async fn handler(v0: &pavex::router::AllowedMethods) -> pavex::Response { + let v1 = pavex::router::default_fallback(v0).await; + ::into_response(v1) + } + struct Next0<'a, T> + where + T: std::future::Future, + { + s_0: &'a pavex::router::AllowedMethods, + next: fn(&'a pavex::router::AllowedMethods) -> T, + } + impl<'a, T> std::future::IntoFuture for Next0<'a, T> + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)(self.s_0) + } + } +} +pub mod route_1 { + pub async fn entrypoint() -> pavex::Response { + let response = wrapping_0().await; + response + } + async fn stage_1() -> pavex::Response { + let response = handler().await; + response + } + async fn wrapping_0() -> pavex::Response { + let v0 = crate::route_1::Next0 { + next: stage_1, + }; + let v1 = pavex::middleware::Next::new(v0); + let v2 = pavex::middleware::wrap_noop(v1).await; + ::into_response(v2) + } + async fn handler() -> pavex::Response { + let v0 = app_dfe43047::get_keyed(); + let v1 = app_dfe43047::handler(v0); + ::into_response(v1) + } + struct Next0 + where + T: std::future::Future, + { + next: fn() -> T, + } + impl std::future::IntoFuture for Next0 + where + T: std::future::Future, + { + type Output = pavex::Response; + type IntoFuture = T; + fn into_future(self) -> Self::IntoFuture { + (self.next)() + } + } +} diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/src/lib.rs b/compiler/ui_tests/reflection/qualified_path_default_with_struct/src/lib.rs new file mode 100644 index 000000000..efc2d89f9 --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/src/lib.rs @@ -0,0 +1,19 @@ +use dep::{Keyed, User}; +use pavex::{blueprint::from, Blueprint}; + +#[pavex::request_scoped] +pub fn get_keyed() -> Keyed { + Keyed(std::marker::PhantomData) +} + +#[pavex::get(path = "/")] +pub fn handler(_keyed: Keyed) -> pavex::Response { + todo!() +} + +pub fn blueprint() -> Blueprint { + let mut bp = Blueprint::new(); + bp.import(from![crate]); + bp.routes(from![crate]); + bp +} diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/src/main.rs b/compiler/ui_tests/reflection/qualified_path_default_with_struct/src/main.rs new file mode 100644 index 000000000..2d1228e7f --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/src/main.rs @@ -0,0 +1,24 @@ +//! This code is generated by `pavex_test_runner`, +//! Do NOT modify it manually. +use app_dfe43047::blueprint; +use pavex_cli_client::{Client, config::Color}; +use pavex_cli_client::commands::generate::GenerateError; + +fn main() -> Result<(), Box> { + let ui_test_dir: std::path::PathBuf = std::env::var("UI_TEST_DIR").unwrap().into(); + let outcome = Client::new() + .color(Color::Always) + .pavex_cli_path(std::env::var("PAVEX_TEST_CLI_PATH").unwrap().into()) + .generate(blueprint(), ui_test_dir.join("generated_app")) + .diagnostics_path("diagnostics.dot".into()) + .execute(); + match outcome { + Ok(_) => {}, + Err(GenerateError::NonZeroExitCode(_)) => { std::process::exit(1); } + Err(e) => { + eprintln!("Failed to invoke `pavex generate`.\n{:?}", e); + std::process::exit(1); + } + } + Ok(()) +} diff --git a/compiler/ui_tests/reflection/qualified_path_default_with_struct/test_config.toml b/compiler/ui_tests/reflection/qualified_path_default_with_struct/test_config.toml new file mode 100644 index 000000000..e676ef54e --- /dev/null +++ b/compiler/ui_tests/reflection/qualified_path_default_with_struct/test_config.toml @@ -0,0 +1,7 @@ +description = "pavex resolves qualified path defaults like ::Assoc on structs" + +[expectations] +codegen = "pass" + +[ephemeral_dependencies] +dep = { path = "dep.rs" } diff --git a/rustdoc/rustdoc_resolver/src/errors.rs b/rustdoc/rustdoc_resolver/src/errors.rs index d4b1d310b..b84d2e637 100644 --- a/rustdoc/rustdoc_resolver/src/errors.rs +++ b/rustdoc/rustdoc_resolver/src/errors.rs @@ -65,6 +65,9 @@ impl std::fmt::Display for TypeResolutionError { TypeResolutionErrorDetails::TypePartResolutionError(source) => { write!(f, "Failed to resolve {}:\n{}", source.role, source.source) } + TypeResolutionErrorDetails::AssociatedTypeResolutionError(e) => { + write!(f, "{e}") + } } } } @@ -83,6 +86,10 @@ pub enum TypeResolutionErrorDetails { GenericKindMismatch(GenericKindMismatch), ItemResolutionError(anyhow::Error), TypePartResolutionError(Box), + /// We couldn't find a concrete `impl Trait for Type` block that defines the associated type. + /// This can happen when the implementation is provided via a blanket impl (e.g., + /// `impl Trait for T`), which we cannot resolve from rustdoc's JSON output. + AssociatedTypeResolutionError(AssociatedTypeResolutionError), } /// A const generic parameter was encountered, which is not yet supported. @@ -119,6 +126,33 @@ pub struct UnsupportedArrayLength { pub len: String, } +/// We couldn't find a concrete `impl Trait for Type` block that defines the associated type. +#[derive(Debug)] +pub struct AssociatedTypeResolutionError { + /// The associated type name (e.g., "Numeric"). + pub assoc_type_name: String, + /// The trait path (e.g., ["enumflags2", "_internal", "RawBitFlags"]). + pub trait_path: Vec, + /// The concrete self type that we resolved. + pub self_type: rustdoc_ir::Type, +} + +impl std::fmt::Display for AssociatedTypeResolutionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let trait_str = self.trait_path.join("::"); + write!( + f, + "Failed to resolve the associated type `{t}::{name}` for `{self_type:?}`. \ + No concrete `impl {t} for {self_type:?}` block was found that defines this \ + associated type. This can happen when the implementation is provided via a blanket \ + impl (e.g., `impl {t} for T`), which we cannot resolve.", + t = trait_str, + name = self.assoc_type_name, + self_type = self.self_type, + ) + } +} + /// A generic argument did not match the expected kind (type vs lifetime vs const). #[derive(Debug)] pub struct GenericKindMismatch { diff --git a/rustdoc/rustdoc_resolver/src/lib.rs b/rustdoc/rustdoc_resolver/src/lib.rs index 37dd6cc57..bbb82125d 100644 --- a/rustdoc/rustdoc_resolver/src/lib.rs +++ b/rustdoc/rustdoc_resolver/src/lib.rs @@ -23,7 +23,7 @@ use rustdoc_ir::Type; /// /// Used to substitute generic parameters with concrete types when resolving /// type aliases and generic instantiations. -#[derive(Default)] +#[derive(Default, Clone)] pub struct GenericBindings { /// Mapping from lifetime parameter names to their resolved lifetime names. pub lifetimes: HashMap, diff --git a/rustdoc/rustdoc_resolver/src/resolve_type.rs b/rustdoc/rustdoc_resolver/src/resolve_type.rs index 13796f8b6..8426181e8 100644 --- a/rustdoc/rustdoc_resolver/src/resolve_type.rs +++ b/rustdoc/rustdoc_resolver/src/resolve_type.rs @@ -11,6 +11,7 @@ use rustdoc_ir::{ GenericLifetimeParameter, PathType, RawPointer, Slice, Tuple, Type, TypeReference, }; use rustdoc_processor::CrateCollection; +use rustdoc_processor::GlobalItemId; use rustdoc_processor::indexing::CrateIndexer; use crate::GenericBindings; @@ -280,6 +281,11 @@ fn _resolve_type( } .params .as_slice(); + // Track resolved type parameters so that defaults for + // later parameters can reference earlier ones. + // E.g. `BitFlags::Numeric>` β€” + // when resolving N's default, T must already be bound. + let mut local_generic_bindings = generic_bindings.clone(); for (i, arg_def) in generic_arg_defs.iter().enumerate() { let generic_argument = match &arg_def.kind { GenericParamDefKind::Lifetime { .. } => { @@ -292,23 +298,21 @@ fn _resolve_type( ) } GenericParamDefKind::Type { default, .. } => { - if let Some(GenericArg::Type(generic_type)) = args.get(i) { + let resolved = if let Some(GenericArg::Type(generic_type)) = + args.get(i) + { if let RustdocType::Generic(generic) = generic_type { if let Some(resolved_type) = generic_bindings.types.get(generic) { - GenericArgument::TypeParameter( - resolved_type.to_owned(), - ) + resolved_type.to_owned() } else { - GenericArgument::TypeParameter(Type::Generic( - Generic { - name: generic.to_owned(), - }, - )) + Type::Generic(Generic { + name: generic.to_owned(), + }) } } else { - GenericArgument::TypeParameter(resolve_type( + resolve_type( generic_type, used_by_package_id, krate_collection, @@ -325,14 +329,14 @@ fn _resolve_type( source, }), ) - })?) + })? } } else if let Some(default) = default { let default = resolve_type( default, &global_type_id.package_id, krate_collection, - generic_bindings, + &local_generic_bindings, alias_resolution, ) .map_err(|source| { @@ -349,12 +353,16 @@ fn _resolve_type( if skip_default(krate_collection, &default) { continue; } - GenericArgument::TypeParameter(default) + default } else { - GenericArgument::TypeParameter(Type::Generic(Generic { + Type::Generic(Generic { name: arg_def.name.clone(), - })) - } + }) + }; + local_generic_bindings + .types + .insert(arg_def.name.clone(), resolved.clone()); + GenericArgument::TypeParameter(resolved) } GenericParamDefKind::Const { .. } => { return Err( @@ -605,14 +613,333 @@ fn _resolve_type( inner: Box::new(resolved), })) } - RustdocType::QualifiedPath { .. } => Err(TypeResolutionErrorDetails::UnsupportedTypeKind( - UnsupportedTypeKind { - kind: "qualified path", - }, - )), + RustdocType::QualifiedPath { + name, + self_type, + trait_, + args: _, + } => { + // 1. Resolve self_type to a concrete type + let resolved_self = resolve_type( + self_type, + used_by_package_id, + krate_collection, + generic_bindings, + alias_resolution, + ) + .map_err(|source| { + TypeResolutionErrorDetails::TypePartResolutionError(Box::new( + TypePartResolutionError { + role: "self type of qualified path".into(), + source, + }, + )) + })?; + + // 2. Get the trait (if present) + let Some(trait_path) = trait_ else { + return Err(TypeResolutionErrorDetails::UnsupportedTypeKind( + UnsupportedTypeKind { + kind: "inherent associated type", + }, + )); + }; + + // 3. Find the associated type in the impl + resolve_associated_type( + &resolved_self, + trait_path, + name, + used_by_package_id, + krate_collection, + generic_bindings, + alias_resolution, + ) + } } } +/// Resolve `::AssocType` by finding the concrete impl block +/// and looking up the associated type definition. +fn resolve_associated_type( + resolved_self: &Type, + trait_path: &rustdoc_types::Path, + assoc_type_name: &str, + used_by_package_id: &PackageId, + krate_collection: &CrateCollection, + generic_bindings: &GenericBindings, + alias_resolution: TypeAliasResolution, +) -> Result { + // Resolve the trait path to get its GlobalItemId and canonical path. + let (trait_global_id, trait_canonical_path) = krate_collection + .get_canonical_path_by_local_type_id(used_by_package_id, &trait_path.id, None) + .map_err(TypeResolutionErrorDetails::ItemResolutionError)?; + + // Try to find the associated type in the concrete type's impls first, + // then fall back to the trait's implementations list. + if let Some(result) = find_trait_assoc_type_in_type_impls( + resolved_self, + trait_canonical_path, + assoc_type_name, + krate_collection, + generic_bindings, + alias_resolution, + )? { + return Ok(result); + } + + if let Some(result) = find_assoc_type_in_trait_impls( + resolved_self, + &trait_global_id, + assoc_type_name, + krate_collection, + generic_bindings, + alias_resolution, + )? { + return Ok(result); + } + + Err(TypeResolutionErrorDetails::AssociatedTypeResolutionError( + AssociatedTypeResolutionError { + assoc_type_name: assoc_type_name.to_owned(), + trait_path: trait_canonical_path.to_vec(), + self_type: resolved_self.clone(), + }, + )) +} + +/// Search through the concrete type's `impls` list to find a matching trait impl +/// and extract the associated type. +fn find_trait_assoc_type_in_type_impls( + resolved_self: &Type, + trait_canonical_path: &[String], + assoc_type_name: &str, + krate_collection: &CrateCollection, + generic_bindings: &GenericBindings, + alias_resolution: TypeAliasResolution, +) -> Result, TypeResolutionErrorDetails> { + // This may not be general enoughβ€”i.e. does the self type in a qualified + // path need to be a `Type::Path`? + // Without looking too deep into it, my gut feeling is "no", but we + // can generalize later. + let Type::Path(self_path) = resolved_self else { + return Ok(None); + }; + + let type_package_id = &self_path.package_id; + let Some(type_crate) = krate_collection.get_crate_by_package_id(type_package_id) else { + return Ok(None); + }; + + // Find the type definition to get its impls list. + let Some(rustdoc_id) = &self_path.rustdoc_id else { + return Ok(None); + }; + let type_item = type_crate.get_item_by_local_type_id(rustdoc_id); + let impls = match &type_item.inner { + ItemEnum::Struct(s) => &s.impls, + ItemEnum::Enum(e) => &e.impls, + ItemEnum::Union(u) => &u.impls, + _ => return Ok(None), + }; + + search_impls_for_assoc_type( + impls, + type_package_id, + trait_canonical_path, + assoc_type_name, + krate_collection, + generic_bindings, + alias_resolution, + ) +} + +/// Search through the trait's `implementations` list to find a matching impl +/// and extract the associated type. +fn find_assoc_type_in_trait_impls( + resolved_self: &Type, + trait_global_id: &GlobalItemId, + assoc_type_name: &str, + krate_collection: &CrateCollection, + generic_bindings: &GenericBindings, + alias_resolution: TypeAliasResolution, +) -> Result, TypeResolutionErrorDetails> { + let trait_item = krate_collection.get_item_by_global_type_id(trait_global_id); + let ItemEnum::Trait(trait_def) = &trait_item.inner else { + return Ok(None); + }; + + let resolved_self = resolved_self.canonicalize(); + + // The trait's implementations are local to the trait's crate. + let trait_package_id = &trait_global_id.package_id; + let Some(trait_crate) = krate_collection.get_crate_by_package_id(trait_package_id) else { + return Ok(None); + }; + for impl_id in &trait_def.implementations { + let Some(impl_item) = trait_crate.maybe_get_item_by_local_type_id(impl_id) else { + continue; + }; + let ItemEnum::Impl(impl_) = &impl_item.inner else { + continue; + }; + if impl_.is_negative { + continue; + } + // Skip generic/blanket impls β€” same reasoning as in `search_impls_for_assoc_type`. + if impl_ + .generics + .params + .iter() + .any(|p| matches!(p.kind, GenericParamDefKind::Type { .. })) + { + continue; + } + // Try to resolve the impl's `for_` type. If resolution fails, skip. + let Ok(resolved_for) = resolve_type( + &impl_.for_, + trait_package_id, + krate_collection, + &GenericBindings::default(), + alias_resolution, + ) else { + continue; + }; + if resolved_for.canonicalize() != resolved_self { + continue; + } + + // Found a matching impl, now look for the associated type. + if let Some(result) = extract_assoc_type_from_impl_items( + &impl_.items, + trait_package_id, + assoc_type_name, + krate_collection, + generic_bindings, + alias_resolution, + )? { + return Ok(Some(result)); + } + } + + Ok(None) +} + +/// Search a list of impl IDs for a matching trait impl and extract the associated type. +fn search_impls_for_assoc_type( + impl_ids: &[rustdoc_types::Id], + impl_crate_package_id: &PackageId, + trait_canonical_path: &[String], + assoc_type_name: &str, + krate_collection: &CrateCollection, + generic_bindings: &GenericBindings, + alias_resolution: TypeAliasResolution, +) -> Result, TypeResolutionErrorDetails> { + let Some(impl_crate) = krate_collection.get_crate_by_package_id(impl_crate_package_id) else { + return Ok(None); + }; + + for impl_id in impl_ids { + let Some(item) = impl_crate.maybe_get_item_by_local_type_id(impl_id) else { + continue; + }; + let ItemEnum::Impl(impl_) = &item.inner else { + continue; + }; + if impl_.is_negative { + continue; + } + // Skip generic/blanket impls (e.g., `impl Trait for T` or + // `impl Trait for T`). We can't verify trait bounds, + // and the associated type value may reference the impl's own + // generic parameters. + if impl_ + .generics + .params + .iter() + .any(|p| matches!(p.kind, GenericParamDefKind::Type { .. })) + { + continue; + } + // Check if this impl is for the right trait. + let Some(impl_trait) = &impl_.trait_ else { + continue; + }; + let Ok((_, impl_trait_path)) = krate_collection.get_canonical_path_by_local_type_id( + impl_crate_package_id, + &impl_trait.id, + None, + ) else { + continue; + }; + if impl_trait_path != trait_canonical_path { + continue; + } + + // Found a matching trait impl, look for the associated type. + if let Some(result) = extract_assoc_type_from_impl_items( + &impl_.items, + impl_crate_package_id, + assoc_type_name, + krate_collection, + generic_bindings, + alias_resolution, + )? { + return Ok(Some(result)); + } + } + + Ok(None) +} + +/// Extract an associated type by name from an impl block's items. +fn extract_assoc_type_from_impl_items( + items: &[rustdoc_types::Id], + package_id: &PackageId, + assoc_type_name: &str, + krate_collection: &CrateCollection, + generic_bindings: &GenericBindings, + alias_resolution: TypeAliasResolution, +) -> Result, TypeResolutionErrorDetails> { + let Some(crate_) = krate_collection.get_crate_by_package_id(package_id) else { + return Ok(None); + }; + + for item_id in items { + let Some(item) = crate_.maybe_get_item_by_local_type_id(item_id) else { + continue; + }; + if item.name.as_deref() != Some(assoc_type_name) { + continue; + } + if let ItemEnum::AssocType { + type_: Some(concrete_type), + .. + } = &item.inner + { + // Resolve the concrete type. It's defined in this crate. + let resolved = resolve_type( + concrete_type, + package_id, + krate_collection, + generic_bindings, + alias_resolution, + ) + .map_err(|source| { + TypeResolutionErrorDetails::TypePartResolutionError(Box::new( + TypePartResolutionError { + role: format!("associated type `{}`", assoc_type_name), + source, + }, + )) + })?; + return Ok(Some(resolved)); + } + } + + Ok(None) +} + /// This is a gigantic hack to work around an issue with `std`'s collections: /// they are all generic over the allocator type, but the default (`alloc::alloc::Global`) /// is a nightly-only type.