From 0d1fc73fbd37abe54318a0c66b496067de3cd7ad Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 12:40:42 +0200 Subject: [PATCH 01/17] refactor: satisfy strict nightly lints without broad allows --- gateway/src/gateway.rs | 124 +++++++++------- live-search/src/app.rs | 267 ++++++++++++++++++++--------------- live-search/src/bootstrap.rs | 49 ++++--- 3 files changed, 262 insertions(+), 178 deletions(-) diff --git a/gateway/src/gateway.rs b/gateway/src/gateway.rs index 19bf215..a61389d 100644 --- a/gateway/src/gateway.rs +++ b/gateway/src/gateway.rs @@ -86,20 +86,52 @@ pub fn build_gateway(modules: Vec>) -> Result]) -> Vec { + modules + .iter() + .map(|module| ServiceInfo { + name: module.name(), + path: module.path(), + description: module.description(), + enabled: module.enabled(), + }) + .collect() +} + +fn enabled_service_router(modules: &[Arc]) -> Router { + modules + .iter() + .filter(|module| module.enabled()) + .fold(Router::new(), |router, module| { + router.nest(&format!("/{}", module.path()), module.router()) + }) +} + +async fn csrf_token_handler( + session: tower_sessions::Session, +) -> Result, crate::auth::error::AppError> { + let token = crate::csrf::get_or_create_token(&session) + .await + .map_err(|error| { + crate::auth::error::AppError::internal( + "csrf token bootstrap", + std::io::Error::other(error), + ) + })?; + Ok(Json(serde_json::json!({ + "csrf_token": token.as_str(), + "header": crate::csrf::TOKEN_HEADER, + }))) +} + +fn gateway_state( modules: Vec>, mut settings: settings::Settings, proxy_upstream_url: String, db_pool: Option, refresh_token_ttl_secs: i64, access_token_ttl_secs: i64, -) -> Result { +) -> Result<(GatewayState, Router), anyhow::Error> { if refresh_token_ttl_secs <= 0 || access_token_ttl_secs <= 0 { anyhow::bail!("gateway token TTLs must be positive"); } @@ -109,40 +141,51 @@ pub fn build_gateway_with_settings( settings.access_token_ttl_secs = access_token_ttl_secs; let (tx, _rx) = broadcast::channel(settings.sse_broadcast_buffer); - - let service_infos: Vec = modules - .iter() - .map(|module| ServiceInfo { - name: module.name(), - path: module.path(), - description: module.description(), - enabled: module.enabled(), - }) - .collect(); - - let mut service_router: Router = Router::new(); - for module in &modules { - if module.enabled() { - service_router = service_router.nest(&format!("/{}", module.path()), module.router()); - } - } - + let services = service_infos(&modules); + let service_router = enabled_service_router(&modules); let http_client = reqwest::Client::builder() .timeout(Duration::from_secs(5)) .build() .context("failed to build reqwest client")?; - let state = GatewayState { - tx, - services: service_infos, + Ok(( + GatewayState { + tx, + services, + modules, + settings, + proxy_upstream_url: proxy_upstream_url.into(), + db_pool, + refresh_token_ttl_secs, + access_token_ttl_secs, + http_client, + }, + service_router, + )) +} + +/// Compose every `ServiceModule` with pre-loaded settings and runtime state. +/// +/// # Errors +/// Returns an error if TTLs are invalid, the HTTP client cannot be built, or +/// rate-limiter configuration fails. +#[instrument(skip(modules, settings, db_pool))] +pub fn build_gateway_with_settings( + modules: Vec>, + mut settings: settings::Settings, + proxy_upstream_url: String, + db_pool: Option, + refresh_token_ttl_secs: i64, + access_token_ttl_secs: i64, +) -> Result { + let (state, service_router) = gateway_state( modules, settings, - proxy_upstream_url: proxy_upstream_url.into(), + proxy_upstream_url, db_pool, refresh_token_ttl_secs, access_token_ttl_secs, - http_client, - }; + )?; let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); @@ -222,23 +265,6 @@ pub fn build_gateway_with_settings( let session_router = crate::session::router::().route_layer(csrf_middleware); - async fn csrf_token_handler( - session: tower_sessions::Session, - ) -> Result, crate::auth::error::AppError> { - let token = crate::csrf::get_or_create_token(&session) - .await - .map_err(|e| { - crate::auth::error::AppError::internal( - "csrf token bootstrap", - std::io::Error::other(e), - ) - })?; - Ok(Json(serde_json::json!({ - "csrf_token": token.as_str(), - "header": crate::csrf::TOKEN_HEADER, - }))) - } - let app = Router::new() .route("/auth/csrf", get(csrf_token_handler)) .merge(login_router) diff --git a/live-search/src/app.rs b/live-search/src/app.rs index fdbf091..83f30aa 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -32,10 +32,6 @@ use crate::styles; /// # Errors /// Returns a server-function error for invalid input, unavailable application /// state, or a database failure. -#[expect( - clippy::unused_async, - reason = "The server body is async only in the SSR build generated by #[server]" -)] #[server(endpoint = "search")] pub async fn search(query: String) -> Result>, ServerFnError> { #[cfg(feature = "ssr")] @@ -120,6 +116,10 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { } #[must_use] +#[expect( + clippy::must_use_candidate, + reason = "Leptos #[component] generates a wrapper that nightly Clippy evaluates separately" +)] #[component] pub fn App() -> impl IntoView { provide_meta_context(); @@ -150,6 +150,10 @@ pub fn App() -> impl IntoView { } } +#[expect( + clippy::empty_enums, + reason = "Leptos #[component] generates an empty typed-builder state enum" +)] #[component] fn SearchErrorBoundary(children: Children) -> impl IntoView { view! { @@ -176,8 +180,8 @@ pub struct SearchResultRow { } #[expect( - clippy::missing_errors_doc, - reason = "Leptos component macro expansion looks fallible although the component API is not" + clippy::empty_enums, + reason = "Leptos #[component] generates an empty typed-builder state enum" )] #[component] fn TitleLinkCellRenderer( @@ -211,6 +215,10 @@ fn result_row_renderer( } #[must_use] +#[expect( + clippy::must_use_candidate, + reason = "Leptos #[component] generates a wrapper that nightly Clippy evaluates separately" +)] #[component] pub fn SearchPage() -> impl IntoView { let (query, set_query) = signal(String::new()); @@ -325,117 +333,150 @@ struct LiveResult { snippet: Arc, } -#[must_use] -#[component] -pub fn LiveFeedPage() -> impl IntoView { - let results = RwSignal::new(VecDeque::::new()); - let connected = RwSignal::new(false); - - #[cfg(target_arch = "wasm32")] - { - let stop = RwSignal::new(false); - let stop_cleanup = stop; - on_cleanup(move || stop_cleanup.set(true)); - - leptos::task::spawn_local(async move { - use futures::{StreamExt, stream}; - use gloo_net::eventsource::State; - use gloo_timers::future::sleep; - use leptos::logging; - use std::time::Duration; - - loop { - if stop.get() { - logging::log!("SSE live feed stopped"); - return; +#[cfg(target_arch = "wasm32")] +fn apply_sse_event( + event: SseEvent, + results: RwSignal>, + connected: RwSignal, +) { + use leptos::logging; + + match event { + SseEvent::Connected { .. } => connected.set(true), + SseEvent::SearchResult { + title, + url, + snippet, + } => { + results.update(|items| { + if items.len() >= 200 { + items.pop_front(); } + items.push_back(LiveResult { + title, + url, + snippet, + }); + }); + } + SseEvent::StreamLagged { skipped } => { + logging::warn!("SSE stream lagged by {skipped} messages"); + } + } +} - match gloo_net::eventsource::futures::EventSource::new("/api/events") { - Ok(mut event_source) => { - let connected_stream = event_source.subscribe("connected"); - let result_stream = event_source.subscribe("search_result"); - let lagged_stream = event_source.subscribe("stream_lagged"); - - match (connected_stream, result_stream, lagged_stream) { - (Ok(connected_events), Ok(result_events), Ok(lagged_events)) => { - while event_source.state() == State::Connecting && !stop.get() { - sleep(Duration::from_millis(25)).await; - } +#[cfg(target_arch = "wasm32")] +async fn consume_event_source( + event_source: &mut gloo_net::eventsource::futures::EventSource, + results: RwSignal>, + connected: RwSignal, + stop: RwSignal, +) { + use futures::{StreamExt, stream}; + use gloo_net::eventsource::State; + use gloo_timers::future::sleep; + use leptos::logging; + use std::time::Duration; + + let connected_stream = event_source.subscribe("connected"); + let result_stream = event_source.subscribe("search_result"); + let lagged_stream = event_source.subscribe("stream_lagged"); + let (Ok(connected_events), Ok(result_events), Ok(lagged_events)) = + (connected_stream, result_stream, lagged_stream) + else { + logging::error!("Failed to subscribe to named SSE events"); + return; + }; - if stop.get() { - return; - } + while event_source.state() == State::Connecting && !stop.get() { + sleep(Duration::from_millis(25)).await; + } + if stop.get() { + return; + } + if event_source.state() != State::Open { + logging::warn!("SSE connection closed before opening"); + return; + } - if event_source.state() == State::Open { - let data_events = stream::select(result_events, lagged_events); - let mut all_events = - stream::select(connected_events, data_events); - while let Some(result) = all_events.next().await { - if stop.get() { - return; - } - match result { - Ok((_event_type, msg)) => { - let Some(data) = msg.data().as_string() else { - logging::warn!( - "SSE message had non-string data" - ); - continue; - }; - match serde_json::from_str::(&data) { - Ok(SseEvent::Connected { .. }) => { - connected.set(true); - } - Ok(SseEvent::SearchResult { - title, - url, - snippet, - }) => { - results.update(|r| { - if r.len() >= 200 { - r.pop_front(); - } - r.push_back(LiveResult { - title, - url, - snippet, - }); - }); - } - Ok(SseEvent::StreamLagged { skipped }) => { - logging::warn!( - "SSE stream lagged by {skipped} messages" - ); - } - Err(e) => { - logging::warn!( - "Invalid SSE message: {e:?}" - ); - } - } - } - Err(e) => { - logging::warn!("SSE stream error: {e:?}"); - break; - } - } - } - } else { - logging::warn!("SSE connection closed before opening"); - } - } - _ => logging::error!("Failed to subscribe to named SSE events"), - } - connected.set(false); - event_source.close(); - } - Err(e) => logging::warn!("Failed to create EventSource: {e}"), + let data_events = stream::select(result_events, lagged_events); + let mut all_events = stream::select(connected_events, data_events); + while let Some(result) = all_events.next().await { + if stop.get() { + return; + } + match result { + Ok((_event_type, msg)) => { + let Some(data) = msg.data().as_string() else { + logging::warn!("SSE message had non-string data"); + continue; + }; + match serde_json::from_str::(&data) { + Ok(event) => apply_sse_event(event, results, connected), + Err(error) => logging::warn!("Invalid SSE message: {error:?}"), } + } + Err(error) => { + logging::warn!("SSE stream error: {error:?}"); + return; + } + } + } +} - sleep(Duration::from_secs(2)).await; +#[cfg(target_arch = "wasm32")] +async fn run_live_feed( + results: RwSignal>, + connected: RwSignal, + stop: RwSignal, +) { + use gloo_net::eventsource::futures::EventSource; + use gloo_timers::future::sleep; + use leptos::logging; + use std::time::Duration; + + loop { + if stop.get() { + logging::log!("SSE live feed stopped"); + return; + } + + match EventSource::new("/api/events") { + Ok(mut event_source) => { + consume_event_source(&mut event_source, results, connected, stop).await; + connected.set(false); + event_source.close(); } - }); + Err(error) => logging::warn!("Failed to create EventSource: {error}"), + } + + if stop.get() { + return; + } + sleep(Duration::from_secs(2)).await; } +} + +#[cfg(target_arch = "wasm32")] +fn start_live_feed(results: RwSignal>, connected: RwSignal) { + let stop = RwSignal::new(false); + let stop_cleanup = stop; + on_cleanup(move || stop_cleanup.set(true)); + leptos::task::spawn_local(run_live_feed(results, connected, stop)); +} + +#[must_use] +#[expect( + clippy::must_use_candidate, + reason = "Leptos #[component] generates a wrapper that nightly Clippy evaluates separately" +)] +#[component] +pub fn LiveFeedPage() -> impl IntoView { + let results = RwSignal::new(VecDeque::::new()); + let connected = RwSignal::new(false); + + #[cfg(target_arch = "wasm32")] + start_live_feed(results, connected); view! {

"Live Feed"

@@ -465,12 +506,12 @@ pub fn LiveFeedPage() -> impl IntoView { } else { items .iter() - .map(|r| { + .map(|result| { view! {
-

{r.title.clone()}

-

{r.snippet.clone()}

- {r.url.clone()} +

{result.title.clone()}

+

{result.snippet.clone()}

+ {result.url.clone()}
} }) diff --git a/live-search/src/bootstrap.rs b/live-search/src/bootstrap.rs index 4eb2ed2..7f119b8 100644 --- a/live-search/src/bootstrap.rs +++ b/live-search/src/bootstrap.rs @@ -100,6 +100,32 @@ async fn fallback_handler(uri: Uri) -> impl IntoResponse { (StatusCode::NOT_FOUND, format!("Not found: {uri}")) } +fn spawn_pg_listener( + tasks: &mut JoinSet>, + pool: sqlx::PgPool, + tx: broadcast::Sender, + cache: CacheHandle, + shutdown: CancellationToken, +) { + let listener_span = tracing::info_span!("pg_listener"); + tasks.spawn( + async move { + db::run_pg_listener(pool, tx, cache, shutdown).await; + Ok(()) + } + .instrument(listener_span), + ); +} + +fn listener_addr(default_port: u16) -> SocketAddr { + let port = std::env::var("PORT") + .or_else(|_| std::env::var("LIVE_SEARCH_PORT")) + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(default_port); + SocketAddr::from(([0, 0, 0, 0], port)) +} + /// Bootstraps all subsystems and starts the HTTP server. /// /// # Errors @@ -150,16 +176,12 @@ pub async fn run() -> anyhow::Result { let shutdown = CancellationToken::new(); let mut tasks = JoinSet::new(); - let listener_token = shutdown.child_token(); - let pool_for_listener = raw_pool.clone(); - let listener_span = tracing::info_span!("pg_listener"); - let cache_for_listener = cache_handle.clone(); - tasks.spawn( - async move { - db::run_pg_listener(pool_for_listener, tx, cache_for_listener, listener_token).await; - Ok(()) - } - .instrument(listener_span), + spawn_pg_listener( + &mut tasks, + raw_pool.clone(), + tx, + cache_handle.clone(), + shutdown.child_token(), ); let conf = get_configuration(None).context("failed to read Leptos configuration")?; @@ -217,12 +239,7 @@ pub async fn run() -> anyhow::Result { let router: Router<()> = router.with_state(leptos_options); - let port: u16 = std::env::var("PORT") - .or_else(|_| std::env::var("LIVE_SEARCH_PORT")) - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(cfg.live_search.port); - let addr = SocketAddr::from(([0, 0, 0, 0], port)); + let addr = listener_addr(cfg.live_search.port); tracing::info!("Live search server listening on {addr}"); let listener = tokio::net::TcpListener::bind(addr) From d42ad52bdf1b4a96ab6e79a0033027635e101843 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 12:45:23 +0200 Subject: [PATCH 02/17] fix: finish strict nightly lint cleanup --- live-search/src/app.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/live-search/src/app.rs b/live-search/src/app.rs index 83f30aa..9610f59 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -22,6 +22,11 @@ use crate::db::SearchResult; use crate::events::SseEvent; use crate::styles; +#[expect( + clippy::empty_enums, + reason = "Leptos #[component] typed-builder macro generates empty state enums in this module" +)] + /// Search `search_results` using PostgreSQL FTS with a trigram fallback. /// /// The `title % $1` branch is backed by the `pg_trgm` GIN index created by @@ -150,10 +155,6 @@ pub fn App() -> impl IntoView { } } -#[expect( - clippy::empty_enums, - reason = "Leptos #[component] generates an empty typed-builder state enum" -)] #[component] fn SearchErrorBoundary(children: Children) -> impl IntoView { view! { @@ -179,10 +180,6 @@ pub struct SearchResultRow { pub url: String, } -#[expect( - clippy::empty_enums, - reason = "Leptos #[component] generates an empty typed-builder state enum" -)] #[component] fn TitleLinkCellRenderer( class: String, From ab8bac5910171adaa4833967c99d5d5f9cf3ece8 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 12:46:07 +0200 Subject: [PATCH 03/17] fix: remove unnecessary gateway mutability --- gateway/src/gateway.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/src/gateway.rs b/gateway/src/gateway.rs index a61389d..ab45581 100644 --- a/gateway/src/gateway.rs +++ b/gateway/src/gateway.rs @@ -172,7 +172,7 @@ fn gateway_state( #[instrument(skip(modules, settings, db_pool))] pub fn build_gateway_with_settings( modules: Vec>, - mut settings: settings::Settings, + settings: settings::Settings, proxy_upstream_url: String, db_pool: Option, refresh_token_ttl_secs: i64, From fe7c0507d4a0bd6aa6bc3091acb4a8a487f26238 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 12:48:25 +0200 Subject: [PATCH 04/17] fix: address remaining strict nightly diagnostics --- live-search/src/app.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/live-search/src/app.rs b/live-search/src/app.rs index 9610f59..f629618 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -4,6 +4,11 @@ //! - `/` — [`SearchPage`] with full-text + trigram search. //! - `/live` — [`LiveFeedPage`] with named Server-Sent Events. +#![expect( + clippy::empty_enums, + reason = "Leptos #[component] typed-builder macro generates empty state enums in this module" +)] + use std::collections::VecDeque; use std::sync::Arc; From 59fc6a2edf635be8c4e8ad461d261b262c9dd48f Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:05:20 +0200 Subject: [PATCH 05/17] fix: address final strict nightly diagnostics --- live-search/src/app.rs | 5 ----- live-search/src/bootstrap.rs | 26 +++++++++++++++----------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/live-search/src/app.rs b/live-search/src/app.rs index f629618..924cb29 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -27,11 +27,6 @@ use crate::db::SearchResult; use crate::events::SseEvent; use crate::styles; -#[expect( - clippy::empty_enums, - reason = "Leptos #[component] typed-builder macro generates empty state enums in this module" -)] - /// Search `search_results` using PostgreSQL FTS with a trigram fallback. /// /// The `title % $1` branch is backed by the `pg_trgm` GIN index created by diff --git a/live-search/src/bootstrap.rs b/live-search/src/bootstrap.rs index 7f119b8..bcb8475 100644 --- a/live-search/src/bootstrap.rs +++ b/live-search/src/bootstrap.rs @@ -126,6 +126,20 @@ fn listener_addr(default_port: u16) -> SocketAddr { SocketAddr::from(([0, 0, 0, 0], port)) } +fn database_settings(config: &rwf_config::Config) -> (String, db::PoolTunables) { + let database_url = std::env::var("DATABASE_URL") + .ok() + .unwrap_or_else(|| config.live_search.database_url.clone()); + let pool_tunables = db::PoolTunables { + max_connections: config.live_search.pool_max_connections, + min_connections: config.live_search.pool_min_connections, + acquire_timeout_secs: config.live_search.pool_acquire_timeout_secs, + idle_timeout_secs: config.live_search.pool_idle_timeout_secs, + max_lifetime_secs: config.live_search.pool_max_lifetime_secs, + }; + (database_url, pool_tunables) +} + /// Bootstraps all subsystems and starts the HTTP server. /// /// # Errors @@ -135,17 +149,7 @@ pub async fn run() -> anyhow::Result { init_tracing(); let cfg = rwf_config::Config::load().context("failed to load workspace config")?; - let database_url = std::env::var("DATABASE_URL") - .ok() - .unwrap_or_else(|| cfg.live_search.database_url.clone()); - - let pool_tunables = db::PoolTunables { - max_connections: cfg.live_search.pool_max_connections, - min_connections: cfg.live_search.pool_min_connections, - acquire_timeout_secs: cfg.live_search.pool_acquire_timeout_secs, - idle_timeout_secs: cfg.live_search.pool_idle_timeout_secs, - max_lifetime_secs: cfg.live_search.pool_max_lifetime_secs, - }; + let (database_url, pool_tunables) = database_settings(&cfg); tracing::info!("{}", cfg.live_search.connection_budget_summary()); let raw_pool = db::create_pool(&database_url, &pool_tunables) From 4a7d5d7fff4f43c4a1b98173abfc11a92883d27f Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:09:54 +0200 Subject: [PATCH 06/17] fix: satisfy strict e2e lint checks --- e2e-tests/src/common/chromium.rs | 8 +++++--- e2e-tests/src/common/db.rs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/e2e-tests/src/common/chromium.rs b/e2e-tests/src/common/chromium.rs index c7f70b0..5ceed7c 100644 --- a/e2e-tests/src/common/chromium.rs +++ b/e2e-tests/src/common/chromium.rs @@ -171,9 +171,11 @@ pub async fn setup() -> Result { } /// Tear down a [`TestContext`] by closing the page and browser in reverse -/// creation order. Cleanup errors are logged via `eprintln!` (stderr, -/// captured by the test harness per-test and displayed only on failure) -/// but never mask the test assertion that already ran. +/// creation order. +/// +/// Cleanup errors are logged via `eprintln!` (stderr, captured by the test +/// harness per-test and displayed only on failure), but never mask the test +/// assertion that already ran. pub async fn teardown(ctx: TestContext) { let TestContext { mut browser, diff --git a/e2e-tests/src/common/db.rs b/e2e-tests/src/common/db.rs index ba487ba..6e57cde 100644 --- a/e2e-tests/src/common/db.rs +++ b/e2e-tests/src/common/db.rs @@ -22,7 +22,7 @@ impl std::fmt::Debug for TestEnv { f.debug_struct("TestEnv") .field("pool", &self.pool) .field("connection_string", &self.connection_string) - .field("container", &"") + .field("_container", &"") .finish() } } From bcfa1bc5e68024038242c1b8d9ed41d497ec6942 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:18:30 +0200 Subject: [PATCH 07/17] refactor: split SSE e2e test helpers --- e2e-tests/tests/sse_test.rs | 246 ++++++++++++++++++------------------ 1 file changed, 120 insertions(+), 126 deletions(-) diff --git a/e2e-tests/tests/sse_test.rs b/e2e-tests/tests/sse_test.rs index caf06d4..249eefc 100644 --- a/e2e-tests/tests/sse_test.rs +++ b/e2e-tests/tests/sse_test.rs @@ -12,7 +12,7 @@ use std::time::Duration; use anyhow::Context; use e2e_tests::common::{LiveSearchEnv, SharedServer}; -use futures::StreamExt; +use futures::{Stream, StreamExt}; /// Shared live-search server instance, initialised lazily on first access. static SERVER: SharedServer = SharedServer::new(); @@ -32,6 +32,80 @@ fn test_client() -> anyhow::Result { .context("failed to build reqwest client") } +fn unique_test_title(prefix: &str) -> String { + format!( + "{prefix}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |duration| duration.as_nanos()) + ) +} + +async fn insert_search_result( + pool: &sqlx::PgPool, + title: &str, + url: &str, + snippet: &str, + context: &'static str, +) -> anyhow::Result<()> { + sqlx::query("INSERT INTO search_results (title, url, snippet) VALUES ($1, $2, $3)") + .bind(title) + .bind(url) + .bind(snippet) + .execute(pool) + .await + .context(context)?; + Ok(()) +} + +async fn wait_for_search_result( + stream: &mut S, + buffer: &mut String, + title: &str, + phase: &str, + poll_timeout: Duration, +) -> anyhow::Result<()> +where + S: Stream> + Unpin, + B: AsRef<[u8]>, + E: std::fmt::Display, +{ + let deadline = tokio::time::Instant::now() + Duration::from_secs(10); + loop { + if tokio::time::Instant::now() >= deadline { + anyhow::bail!( + "Timeout waiting for {phase} SSE event. Buffer so far (first 500 chars): {buffer:.500}" + ); + } + + match tokio::time::timeout(poll_timeout, stream.next()).await { + Ok(Some(Ok(chunk))) => { + buffer.push_str(&String::from_utf8_lossy(chunk.as_ref())); + buffer.push('\n'); + if buffer.contains(title) && buffer.contains("SearchResult") { + return Ok(()); + } + } + Ok(Some(Err(error))) => anyhow::bail!("SSE stream error during {phase}: {error}"), + Ok(None) => { + anyhow::bail!("SSE stream ended during {phase}. Buffer: {buffer:.300}"); + } + Err(_) => {} + } + } +} + +async fn cleanup_test_row(pool: &sqlx::PgPool, title: &str, label: &str) { + if let Err(error) = sqlx::query("DELETE FROM search_results WHERE title = $1") + .bind(title) + .execute(pool) + .await + { + eprintln!("warning: failed to delete {label} row '{title}': {error}"); + } +} + // --------------------------------------------------------------------------- // Required integration tests (from spec) // --------------------------------------------------------------------------- @@ -85,13 +159,11 @@ async fn notify_trigger_fires_sse_event() -> anyhow::Result<()> { .await .with_context(|| format!("failed to connect to {conn_str}"))?; - // ── 0. Pre-clean: wipe any leftover e2e-test rows from previous failed runs. sqlx::query("DELETE FROM search_results WHERE title LIKE 'e2e-%' OR title LIKE 'e2e-warmup-%' OR title LIKE 'browser-sse-sentinel-%'") .execute(&pool) .await .ok(); - // ── 1. Open an SSE connection to /api/events ────────────────────── let client = test_client()?; let url = format!("{}/api/events", env.base_url()); let response = client @@ -99,138 +171,60 @@ async fn notify_trigger_fires_sse_event() -> anyhow::Result<()> { .send() .await .with_context(|| format!("failed to GET {url}"))?; - assert_eq!(response.status(), 200); + let mut stream = response.bytes_stream(); - let mut buf = String::new(); + let mut buffer = String::new(); - // ── 2. Insert a warmup row and wait for it to appear in the stream ─ - // - // PostgreSQL NOTIFY is best-effort: if `PgListener` hasn't yet called - // `LISTEN search_results` (cold start, slow CI runner), the NOTIFY is - // dropped silently and the assertion below would time out for the wrong - // reason. We can't observe `PgListener::listen()` directly, but we CAN - // prove the listener is active by inserting a sentinel row and waiting - // for the resulting SSE event. (Waiting for `SseEvent::Connected` is - // NOT sufficient — that event is emitted by the SSE handler itself, - // independent of the broadcast/listener pipeline.) - let warmup_title = format!( - "e2e-warmup-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(0, |d| d.as_nanos()) - ); + // PostgreSQL NOTIFY is best-effort, so first prove PgListener has reached + // LISTEN before inserting the row whose event the test actually asserts. + let warmup_title = unique_test_title("e2e-warmup"); let warmup_url = format!("https://example.com/e2e-warmup/{warmup_title}"); - sqlx::query("INSERT INTO search_results (title, url, snippet) VALUES ($1, $2, $3)") - .bind(&warmup_title) - .bind(&warmup_url) - .bind("SSE warmup row to prove PgListener is LISTEN-ing") - .execute(&pool) - .await - .context("failed to insert warmup row")?; - - let warmup_deadline = tokio::time::Instant::now() + Duration::from_secs(10); - loop { - assert!( - tokio::time::Instant::now() < warmup_deadline, - "Timeout waiting for warmup SSE event — PgListener may not be \ - LISTEN-ing yet. Buffer so far (first 500 chars): {buf:.500}" - ); - match tokio::time::timeout(Duration::from_secs(1), stream.next()).await { - Ok(Some(Ok(chunk))) => { - buf.push_str(&String::from_utf8_lossy(&chunk)); - buf.push('\n'); - if buf.contains(&warmup_title) && buf.contains("SearchResult") { - break; - } - } - Ok(Some(Err(e))) => return Err(anyhow::anyhow!("SSE stream error during warmup: {e}")), - Ok(None) => { - return Err(anyhow::anyhow!( - "SSE stream ended during warmup. Buffer: {buf:.300}" - )); - } - Err(_timeout) => {} // keep waiting - } - } + insert_search_result( + &pool, + &warmup_title, + &warmup_url, + "SSE warmup row to prove PgListener is LISTEN-ing", + "failed to insert warmup row", + ) + .await?; + wait_for_search_result( + &mut stream, + &mut buffer, + &warmup_title, + "warmup", + Duration::from_secs(1), + ) + .await?; println!("Warmup SSE event received — PgListener is LISTEN-ing"); - // ── 3. Insert the real test row ────────────────────────────────── - let title = format!( - "e2e-test-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(0, |d| d.as_nanos()) - ); - // Use a unique URL per test run so the UNIQUE constraint on `url` does - // not reject the INSERT when the test is re-run. + let title = unique_test_title("e2e-test"); let test_url = format!("https://example.com/e2e-test/{title}"); - let snippet = "E2E test snippet for NOTIFY→SSE verification"; + insert_search_result( + &pool, + &title, + &test_url, + "E2E test snippet for NOTIFY→SSE verification", + "failed to insert search result", + ) + .await?; + wait_for_search_result( + &mut stream, + &mut buffer, + &title, + "SearchResult", + Duration::from_secs(2), + ) + .await?; + println!("SearchResult SSE event with title '{title}' received"); - sqlx::query("INSERT INTO search_results (title, url, snippet) VALUES ($1, $2, $3)") - .bind(&title) - .bind(&test_url) - .bind(snippet) - .execute(&pool) - .await - .context("failed to insert search result")?; - - // ── 4. Read SSE events until we see the SearchResult ────────────── - let deadline = tokio::time::Instant::now() + Duration::from_secs(10); - loop { - assert!( - tokio::time::Instant::now() < deadline, - "Timeout waiting for SearchResult SSE event. \ - Buffer so far (first 500 chars): {buf:.500}" - ); - match tokio::time::timeout(Duration::from_secs(2), stream.next()).await { - Ok(Some(Ok(chunk))) => { - buf.push_str(&String::from_utf8_lossy(&chunk)); - buf.push('\n'); - if buf.contains(&title) && buf.contains("SearchResult") { - println!("SearchResult SSE event with title '{title}' received"); - - // Snapshot the event payload for regression detection. - // Fields like `title`, `url`, `snippet` contain the - // inserted values — the snapshot captures the shape. - if let Some(event_json) = parse_sse_json_payload(&buf, &title) { - // unwrap is safe: to_string_pretty only fails on - // non-finite floats, which serde_json::Value from a - // known schema never contains. - event_snapshot(event_json); - } - - // Best-effort cleanup so re-runs don't accumulate rows. - if let Err(e) = sqlx::query("DELETE FROM search_results WHERE title = $1") - .bind(&title) - .execute(&pool) - .await - { - eprintln!("warning: failed to delete e2e-test row '{title}': {e}"); - } - if let Err(e) = sqlx::query("DELETE FROM search_results WHERE title = $1") - .bind(&warmup_title) - .execute(&pool) - .await - { - eprintln!("warning: failed to delete e2e-warmup row '{warmup_title}': {e}"); - } - return Ok(()); - } - } - Ok(Some(Err(e))) => return Err(anyhow::anyhow!("SSE stream error: {e}")), - Ok(None) => { - return Err(anyhow::anyhow!( - "SSE stream ended before SearchResult. Buffer: {buf:.300}" - )); - } - Err(_timeout) => { - // No data in this 2 s window — keep waiting. - } - } + if let Some(event_json) = parse_sse_json_payload(&buffer, &title) { + event_snapshot(event_json); } + + cleanup_test_row(&pool, &title, "e2e-test").await; + cleanup_test_row(&pool, &warmup_title, "e2e-warmup").await; + Ok(()) } /// Try to extract the SSE event JSON for the test row from the accumulated From bf87da6e0f724d9e8841135c802c9b981dbe43e0 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:22:24 +0200 Subject: [PATCH 08/17] ci: align native Clippy with strict lint groups --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb33f58..2fad14f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,11 +139,12 @@ jobs: clippy-target-v2-${{ runner.os }}-rust-1.98.1- - name: Clippy run: | - cargo +1.98.1 clippy --locked --workspace --all-targets -- -D warnings - cargo +1.98.1 clippy --locked -p live-search --features ssr --all-targets -- -D warnings - cargo +1.98.1 clippy --locked -p live-search --features ssr,otel --all-targets -- -D warnings - cargo +1.98.1 clippy --locked -p gateway-example --features otel --all-targets -- -D warnings - cargo +1.98.1 clippy --locked -p i18n-demo --features ssr --all-targets -- -D warnings + lint_args=(-D warnings -D clippy::pedantic -D clippy::nursery) + cargo +1.98.1 clippy --locked --workspace --all-targets -- "${lint_args[@]}" + cargo +1.98.1 clippy --locked -p live-search --features ssr --all-targets -- "${lint_args[@]}" + cargo +1.98.1 clippy --locked -p live-search --features ssr,otel --all-targets -- "${lint_args[@]}" + cargo +1.98.1 clippy --locked -p gateway-example --features otel --all-targets -- "${lint_args[@]}" + cargo +1.98.1 clippy --locked -p i18n-demo --features ssr --all-targets -- "${lint_args[@]}" - name: Report Clippy cache footprint if: always() run: du -sh target/debug 2>/dev/null || true From b8c9a15b8db2ddc752412daae9453d01f855098f Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:24:17 +0200 Subject: [PATCH 09/17] fix: use pedantic-friendly result assertion --- e2e-tests/tests/live_search_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/tests/live_search_test.rs b/e2e-tests/tests/live_search_test.rs index 65fd27a..978e27e 100644 --- a/e2e-tests/tests/live_search_test.rs +++ b/e2e-tests/tests/live_search_test.rs @@ -323,7 +323,7 @@ async fn server_fn_search_returns_results_via_http() -> anyhow::Result<()> { let results = json .as_array() .context("server fn response should be a JSON array")?; - assert!(!results.is_empty()); + assert_ne!(results.len(), 0, "search should return at least one result"); assert!(results.iter().any(|row| { row.get("title") .and_then(serde_json::Value::as_str) From 42f21363a26fe22da999824a69b4845b9d7b6367 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:37:48 +0200 Subject: [PATCH 10/17] fix: bound strict lint exemptions to generated i18n code --- i18n-demo/build.rs | 10 +++++++--- i18n-demo/src/app.rs | 8 ++++++++ live-search/src/app.rs | 5 ----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/i18n-demo/build.rs b/i18n-demo/build.rs index b968f2a..9e86abb 100644 --- a/i18n-demo/build.rs +++ b/i18n-demo/build.rs @@ -26,12 +26,16 @@ fn main() -> Result<(), Box> { translations_infos.rerun_if_locales_changed(); - // Suppress style-only Clippy noise in third-party generated source. The - // strict gate still forbids unwrap/expect/panic/unsafe-policy lints, so - // generated code cannot waive those non-negotiable categories. + // Bound Clippy exemptions to third-party generated source. Handwritten + // source remains governed by rust-skills2's source-policy pass; this + // generated module necessarily contains its own allow attributes, locale + // parse expects, and typed-builder empty enums. let top_level_attrs: proc_macro2::TokenStream = concat!( "#![allow(", "clippy::allow_attributes, ", + "clippy::allow_attributes_without_reason, ", + "clippy::expect_used, ", + "clippy::empty_enums, ", "clippy::pedantic, ", "clippy::module_inception, ", "clippy::use_self, ", diff --git a/i18n-demo/src/app.rs b/i18n-demo/src/app.rs index a42183a..ebf8fae 100644 --- a/i18n-demo/src/app.rs +++ b/i18n-demo/src/app.rs @@ -49,6 +49,10 @@ fn set_document_lang(lang: &str) { const fn set_document_lang(_lang: &str) {} #[must_use] +#[expect( + clippy::must_use_candidate, + reason = "Leptos #[component] generates a wrapper that nightly Clippy evaluates separately" +)] #[component] pub fn App() -> impl IntoView { provide_meta_context(); @@ -70,6 +74,10 @@ pub fn App() -> impl IntoView { } #[must_use] +#[expect( + clippy::must_use_candidate, + reason = "Leptos #[component] generates a wrapper that nightly Clippy evaluates separately" +)] #[component] pub fn Home() -> impl IntoView { let i18n = use_i18n(); diff --git a/live-search/src/app.rs b/live-search/src/app.rs index 924cb29..d3d8e53 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -4,11 +4,6 @@ //! - `/` — [`SearchPage`] with full-text + trigram search. //! - `/live` — [`LiveFeedPage`] with named Server-Sent Events. -#![expect( - clippy::empty_enums, - reason = "Leptos #[component] typed-builder macro generates empty state enums in this module" -)] - use std::collections::VecDeque; use std::sync::Arc; From 51bd2ed18399fa87faf24af5c84337280b4cdb2f Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:41:13 +0200 Subject: [PATCH 11/17] fix: scope Leptos empty-enum expectations --- live-search/src/app.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/live-search/src/app.rs b/live-search/src/app.rs index d3d8e53..83f30aa 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -150,6 +150,10 @@ pub fn App() -> impl IntoView { } } +#[expect( + clippy::empty_enums, + reason = "Leptos #[component] generates an empty typed-builder state enum" +)] #[component] fn SearchErrorBoundary(children: Children) -> impl IntoView { view! { @@ -175,6 +179,10 @@ pub struct SearchResultRow { pub url: String, } +#[expect( + clippy::empty_enums, + reason = "Leptos #[component] generates an empty typed-builder state enum" +)] #[component] fn TitleLinkCellRenderer( class: String, From abb0847f36a80bf6075e1a37c9b7c214854becaf Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:48:20 +0200 Subject: [PATCH 12/17] fix: propagate Leptos lint expectations through component macro --- live-search/src/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/live-search/src/app.rs b/live-search/src/app.rs index 83f30aa..8a57d72 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -150,11 +150,11 @@ pub fn App() -> impl IntoView { } } +#[component] #[expect( clippy::empty_enums, reason = "Leptos #[component] generates an empty typed-builder state enum" )] -#[component] fn SearchErrorBoundary(children: Children) -> impl IntoView { view! { , From 9ad6f1504d4cf8850b26a604c46caf16b4824104 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 13:55:16 +0200 Subject: [PATCH 13/17] refactor: remove unnecessary Leptos component wrappers --- live-search/src/app.rs | 64 +++++++++++++----------------------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/live-search/src/app.rs b/live-search/src/app.rs index 8a57d72..f9c5816 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -150,28 +150,10 @@ pub fn App() -> impl IntoView { } } -#[component] -#[expect( - clippy::empty_enums, - reason = "Leptos #[component] generates an empty typed-builder state enum" -)] -fn SearchErrorBoundary(children: Children) -> impl IntoView { - view! { - -

"Something went wrong."

-

"Try reloading the page to recover."

- - } - }>{children()}
- } -} - #[derive(Debug, TableRow, Clone)] #[table(impl_vec_data_provider)] pub struct SearchResultRow { - #[table(title = "Title", renderer = "TitleLinkCellRenderer")] + #[table(title = "Title")] pub title: String, #[table(title = "Snippet")] pub snippet: String, @@ -179,37 +161,24 @@ pub struct SearchResultRow { pub url: String, } -#[component] -#[expect( - clippy::empty_enums, - reason = "Leptos #[component] generates an empty typed-builder state enum" -)] -fn TitleLinkCellRenderer( - class: String, - value: Signal, - row: RwSignal, - index: usize, -) -> impl IntoView { - let _ = index; - let url = move || row.read().url.clone(); - view! { - - {value} - - } -} - fn result_row_renderer( class: Signal, row: RwSignal, - index: usize, + _index: usize, _selected: Signal, on_select: EventHandler, - columns: RwSignal>, + _columns: RwSignal>, ) -> impl IntoView { + let row = row.get_untracked(); + let title = row.title; + let snippet = row.snippet; + let url = row.url; + view! { - {SearchResultRow::render_row(row, index, columns)} + {title} + {snippet} + {url} } } @@ -272,7 +241,14 @@ pub fn SearchPage() -> impl IntoView { - + +

"Something went wrong."

+

"Try reloading the page to recover."

+ + } + }>

"Enter a query above to search."

@@ -322,7 +298,7 @@ pub fn SearchPage() -> impl IntoView { }) }}
-
+
} } From 7c41e4884b102bf37a1e90c39104e0d12ec98d83 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 14:51:00 +0200 Subject: [PATCH 14/17] fix: remove strict-gate manifest warnings --- e2e-tests/Cargo.toml | 8 +++----- gateway/Cargo.toml | 1 - i18n-demo/Cargo.toml | 7 +++---- live-search/Cargo.toml | 5 ++--- live-search/src/app.rs | 4 +++- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 158fb6f..13f9953 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -78,8 +78,7 @@ opentelemetry_sdk = { workspace = true, optional = true, features = ["testing"] # Test infrastructure testcontainers = { workspace = true } -# Snapshot testing & property-based testing -insta = { workspace = true, features = ["json"] } +# Property-based test helpers are used by the shared test-support library. proptest = { workspace = true } # Shared domain types (SearchResult, etc.) @@ -99,6 +98,5 @@ chrono = { workspace = true } uuid = { workspace = true } [dev-dependencies] -# Anything extra that only the integration tests need (not the lib helpers). -# Currently empty — every dep that test binaries need is already a regular -# dependency, reachable through the library crate. +# Snapshot assertions are used only by integration-test binaries. +insta = { workspace = true, features = ["json"] } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 93929af..3b370ee 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -44,7 +44,6 @@ tower_governor = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } utoipa = { workspace = true } -utoipa-axum = { workspace = true } utoipa-swagger-ui = { workspace = true } uuid = { workspace = true, features = ["v4"] } validator = { workspace = true } diff --git a/i18n-demo/Cargo.toml b/i18n-demo/Cargo.toml index 9682a84..749bf51 100644 --- a/i18n-demo/Cargo.toml +++ b/i18n-demo/Cargo.toml @@ -31,10 +31,9 @@ serde_json = { workspace = true } uuid = { workspace = true, features = ["v4"] } tokio = { workspace = true } tokio-util = { workspace = true } -tower-http = { workspace = true, features = ["trace", "fs"] } +tower-http = { workspace = true, optional = true, features = ["trace", "fs"] } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } -leptos-utils = { workspace = true } +tracing-subscriber = { workspace = true, optional = true, features = ["env-filter"] } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { workspace = true } @@ -46,7 +45,7 @@ proc-macro2 = "1" [features] default = [] -ssr = ["dep:leptos_axum", "leptos/ssr", "leptos_i18n/ssr"] +ssr = ["dep:leptos_axum", "dep:tower-http", "dep:tracing-subscriber", "leptos/ssr", "leptos_i18n/ssr"] hydrate = ["leptos/hydrate", "leptos_i18n/hydrate"] csr = ["leptos/csr", "leptos_i18n/csr"] diff --git a/live-search/Cargo.toml b/live-search/Cargo.toml index 4730440..472adcc 100644 --- a/live-search/Cargo.toml +++ b/live-search/Cargo.toml @@ -56,11 +56,10 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } tower-http = { workspace = true, features = ["trace", "fs"] } thiserror = { workspace = true } -axum-prometheus = { workspace = true } +axum-prometheus = { workspace = true, optional = true } moka = { workspace = true } base64 = { workspace = true } rwf-config = { path = "../crates/config" } -leptos-utils = { workspace = true } # OTel stack — gated behind `otel` feature. opentelemetry = { workspace = true, optional = true } opentelemetry_sdk = { workspace = true, optional = true } @@ -81,7 +80,7 @@ rwf-domain = { workspace = true } [features] default = [] -ssr = ["dep:leptos_axum", "leptos/ssr", "leptos-use/ssr"] +ssr = ["dep:leptos_axum", "dep:axum-prometheus", "leptos/ssr", "leptos-use/ssr"] hydrate = ["leptos/hydrate"] test-seams = [] otel = [ diff --git a/live-search/src/app.rs b/live-search/src/app.rs index f9c5816..8a45bd8 100644 --- a/live-search/src/app.rs +++ b/live-search/src/app.rs @@ -176,7 +176,9 @@ fn result_row_renderer( view! { - {title} + + {title} + {snippet} {url} From 9875f3fe449d1dabf39b81a809da53e5f557b722 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 14:54:21 +0200 Subject: [PATCH 15/17] chore: refresh lockfile after dependency cleanup --- Cargo.lock | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2910cf..ba2ae88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1786,7 +1786,6 @@ dependencies = [ "tracing-opentelemetry", "tracing-subscriber", "utoipa", - "utoipa-axum", "utoipa-swagger-ui", "uuid", "validator", @@ -2301,7 +2300,6 @@ dependencies = [ "console_error_panic_hook", "futures", "leptos", - "leptos-utils", "leptos_axum", "leptos_i18n", "leptos_i18n_build", @@ -3625,7 +3623,6 @@ dependencies = [ "leptos", "leptos-struct-table", "leptos-use 0.19.2", - "leptos-utils", "leptos_axum", "leptos_meta", "leptos_router", @@ -4219,12 +4216,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pastey" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" - [[package]] name = "pathdiff" version = "0.2.3" @@ -6880,19 +6871,6 @@ dependencies = [ "utoipa-gen", ] -[[package]] -name = "utoipa-axum" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865319162dc3d0032e1e40ed11bd49d5b78e8ef576740589fd6823e2e9977423" -dependencies = [ - "axum", - "pastey", - "tower-layer", - "tower-service", - "utoipa", -] - [[package]] name = "utoipa-gen" version = "6.0.1" From ef99447089b934d6ba0d63d0ccfb14278bac827a Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 15:09:42 +0200 Subject: [PATCH 16/17] fix: align feature-gated dependencies with strict manifest lints --- Cargo.lock | 1 - e2e-tests/Cargo.toml | 4 ---- live-search/Cargo.toml | 3 ++- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba2ae88..5a62934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1389,7 +1389,6 @@ dependencies = [ "gateway-example", "insta", "leptos", - "leptos-utils", "leptos_axum", "live-search", "opentelemetry", diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 13f9953..5088483 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -86,10 +86,6 @@ rwf-domain = { workspace = true } # Import the real SearchResult type and SSR modules for in-process server live-search = { path = "../live-search", features = ["ssr", "test-seams"] } -# Shared Leptos axum helpers (doubled-prefix server-fn probe) for in-process -# test launchers -leptos-utils = { workspace = true } - # Gateway example for in-process HTTP tests gateway-example = { path = "../gateway" } diff --git a/live-search/Cargo.toml b/live-search/Cargo.toml index 472adcc..3589b01 100644 --- a/live-search/Cargo.toml +++ b/live-search/Cargo.toml @@ -80,10 +80,11 @@ rwf-domain = { workspace = true } [features] default = [] -ssr = ["dep:leptos_axum", "dep:axum-prometheus", "leptos/ssr", "leptos-use/ssr"] +ssr = ["dep:leptos_axum", "leptos/ssr", "leptos-use/ssr"] hydrate = ["leptos/hydrate"] test-seams = [] otel = [ + "dep:axum-prometheus", "dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-otlp", From fcee681dde12c9faf868512eb1b76a4c16d9e877 Mon Sep 17 00:00:00 2001 From: codeandsolder-git-wrangler Date: Tue, 29 Sep 2026 15:29:13 +0200 Subject: [PATCH 17/17] fix: patch transitive Rust future incompatibilities --- Cargo.lock | 4 - Cargo.toml | 13 +- third-party/README.md | 29 + third-party/manyhow/.gitignore | 2 + third-party/manyhow/Cargo.toml | 114 +++ third-party/manyhow/Cargo.toml.orig | 55 ++ third-party/manyhow/LICENSE-APACHE | 201 +++++ third-party/manyhow/LICENSE-MIT | 21 + third-party/manyhow/README.md | 122 +++ third-party/manyhow/rustfmt.toml | 15 + third-party/manyhow/src/error.rs | 515 +++++++++++ third-party/manyhow/src/lib.rs | 850 ++++++++++++++++++ third-party/manyhow/src/macro_rules.rs | 323 +++++++ third-party/manyhow/src/parse_to_tokens.rs | 172 ++++ third-party/manyhow/src/span_ranged.rs | 208 +++++ third-party/proc-macro-error2/.gitignore | 4 + third-party/proc-macro-error2/CHANGELOG.md | 180 ++++ third-party/proc-macro-error2/Cargo.toml | 91 ++ third-party/proc-macro-error2/Cargo.toml.orig | 41 + third-party/proc-macro-error2/LICENSE-APACHE | 201 +++++ third-party/proc-macro-error2/LICENSE-MIT | 21 + third-party/proc-macro-error2/README.md | 250 ++++++ .../proc-macro-error2/src/diagnostic.rs | 360 ++++++++ third-party/proc-macro-error2/src/dummy.rs | 151 ++++ .../proc-macro-error2/src/imp/delegate.rs | 68 ++ .../proc-macro-error2/src/imp/fallback.rs | 30 + third-party/proc-macro-error2/src/lib.rs | 565 ++++++++++++ third-party/proc-macro-error2/src/macros.rs | 288 ++++++ third-party/proc-macro-error2/src/sealed.rs | 3 + 29 files changed, 4887 insertions(+), 10 deletions(-) create mode 100644 third-party/README.md create mode 100644 third-party/manyhow/.gitignore create mode 100644 third-party/manyhow/Cargo.toml create mode 100644 third-party/manyhow/Cargo.toml.orig create mode 100644 third-party/manyhow/LICENSE-APACHE create mode 100644 third-party/manyhow/LICENSE-MIT create mode 100644 third-party/manyhow/README.md create mode 100644 third-party/manyhow/rustfmt.toml create mode 100644 third-party/manyhow/src/error.rs create mode 100644 third-party/manyhow/src/lib.rs create mode 100644 third-party/manyhow/src/macro_rules.rs create mode 100644 third-party/manyhow/src/parse_to_tokens.rs create mode 100644 third-party/manyhow/src/span_ranged.rs create mode 100644 third-party/proc-macro-error2/.gitignore create mode 100644 third-party/proc-macro-error2/CHANGELOG.md create mode 100644 third-party/proc-macro-error2/Cargo.toml create mode 100644 third-party/proc-macro-error2/Cargo.toml.orig create mode 100644 third-party/proc-macro-error2/LICENSE-APACHE create mode 100644 third-party/proc-macro-error2/LICENSE-MIT create mode 100644 third-party/proc-macro-error2/README.md create mode 100644 third-party/proc-macro-error2/src/diagnostic.rs create mode 100644 third-party/proc-macro-error2/src/dummy.rs create mode 100644 third-party/proc-macro-error2/src/imp/delegate.rs create mode 100644 third-party/proc-macro-error2/src/imp/fallback.rs create mode 100644 third-party/proc-macro-error2/src/lib.rs create mode 100644 third-party/proc-macro-error2/src/macros.rs create mode 100644 third-party/proc-macro-error2/src/sealed.rs diff --git a/Cargo.lock b/Cargo.lock index 5a62934..e988d78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3685,8 +3685,6 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "manyhow" version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" dependencies = [ "manyhow-macros", "proc-macro2", @@ -4382,8 +4380,6 @@ dependencies = [ [[package]] name = "proc-macro-error2" version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ "proc-macro-error-attr2", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 3dd4b76..f986ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,13 @@ [workspace] resolver = "3" members = ["crates/config", "crates/domain", "crates/leptos-utils", "gateway", "live-search", "i18n-demo", "e2e-tests"] +exclude = ["third-party/manyhow", "third-party/proc-macro-error2"] + +[patch.crates-io] +# Temporary source-compatible fixes for dependencies currently used by Leptos. +# Remove each patch once crates.io/upstream contains the corresponding fix. +manyhow = { path = "third-party/manyhow" } +proc-macro-error2 = { path = "third-party/proc-macro-error2" } [workspace.package] version = "0.1.0" @@ -51,7 +58,6 @@ leptos-use = "0.19" leptos-struct-table = "0.19" lepticons = "0.13" utoipa = "6" -utoipa-axum = "0.3" # `axum` enables the SwaggerUi router builder; `vendored` bundles Swagger UI assets so Docker builds don't need network. utoipa-swagger-ui = { version = "10", features = ["axum", "vendored"] } opentelemetry = "0.32" @@ -76,11 +82,6 @@ config = { version = "0.15", default-features = false, features = ["toml"] } # Shared domain types (no framework dependencies). rwf-domain = { path = "crates/domain" } -# Deprecated compatibility shim around `leptos_axum::handle_server_fns`. -# New code should call leptos_axum directly; this remains only for source -# compatibility until downstream users have migrated. -leptos-utils = { path = "crates/leptos-utils" } - [workspace.lints.rust] unsafe_code = "deny" rust_2024_compatibility = { level = "deny", priority = -1 } diff --git a/third-party/README.md b/third-party/README.md new file mode 100644 index 0000000..9c285ac --- /dev/null +++ b/third-party/README.md @@ -0,0 +1,29 @@ +# Temporary compatibility patches + +These crates are exact copies of the crates.io releases selected by `Cargo.lock`, +with narrowly scoped source-compatible fixes for Rust future-incompatibility +warnings. They are wired through the root `[patch.crates-io]` table and are +excluded from workspace membership. + +## manyhow 0.11.4 + +Upstream crate: `manyhow 0.11.4`. + +Patch: remove the trailing semicolon from each `bail!` expansion. The macro +still expands to the same early `return Err(...)`, but callers can use it in +expression position without triggering Rust issue #79813 +(`semicolon_in_expressions_from_macros`). + +This removes the future-incompatibility warnings currently attributed to +`attribute-derive-macro 0.10.5`. + +## proc-macro-error2 2.0.1 + +Upstream crate: `proc-macro-error2 2.0.1`. + +Patch: make the crate's `extern crate proc_macro` public because its hidden +`__export` module publicly re-exports `proc_macro`. This is the compiler's +suggested source-compatible fix for Rust issue #127909 / E0365. + +`proc-macro-error2` is unmaintained; prefer removing this patch when the +upstream dependency chain migrates to a maintained replacement. diff --git a/third-party/manyhow/.gitignore b/third-party/manyhow/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/third-party/manyhow/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/third-party/manyhow/Cargo.toml b/third-party/manyhow/Cargo.toml new file mode 100644 index 0000000..5909456 --- /dev/null +++ b/third-party/manyhow/Cargo.toml @@ -0,0 +1,114 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "manyhow" +version = "0.11.4" +build = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "proc macro error handling à la anyhow x proc-macro-error" +documentation = "https://docs.rs/manyhow" +readme = "README.md" +keywords = [ + "proc-macro", + "error", + "error-handling", +] +categories = [ + "development-tools::procedural-macro-helpers", + "rust-patterns", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/ModProg/manyhow" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[package.metadata.release] +shared-version = true + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +replace = """ + +## [{{version}}] - {{date}}""" +search = '## \[Unreleased\]' + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +replace = """ +[unreleased]: $1/{{tag_name}}...HEAD +[{{version}}]: $1/$2...{{tag_name}}""" +search = '\[unreleased\]: (.*)/(v.*)\.\.\.HEAD' + +[lib] +name = "manyhow" +path = "src/lib.rs" + +[dependencies.darling_core] +version = "0.20.1" +optional = true + +[dependencies.macros] +version = "0.11.4" +optional = true +package = "manyhow-macros" + +[dependencies.proc-macro2] +version = "1.0.60" + +[dependencies.quote] +version = "1" + +[dependencies.syn1] +version = "1" +features = ["printing"] +optional = true +default-features = false +package = "syn" + +[dependencies.syn2] +version = "2" +features = [ + "printing", + "parsing", +] +optional = true +default-features = false +package = "syn" + +[dev-dependencies.proc-macro-utils] +version = "0.8.0" + +[dev-dependencies.proc-macro2] +version = "1" +features = ["span-locations"] + +[dev-dependencies.syn2] +version = "2" +features = ["full"] +package = "syn" + +[features] +darling = ["darling_core"] +default = [ + "syn", + "macros", +] +syn = ["syn2"] diff --git a/third-party/manyhow/Cargo.toml.orig b/third-party/manyhow/Cargo.toml.orig new file mode 100644 index 0000000..2f5c8bc --- /dev/null +++ b/third-party/manyhow/Cargo.toml.orig @@ -0,0 +1,55 @@ +[package] +name = "manyhow" +version = "0.11.4" +edition = "2021" +categories = ["development-tools::procedural-macro-helpers", "rust-patterns"] +description = "proc macro error handling à la anyhow x proc-macro-error" +keywords = ["proc-macro", "error", "error-handling"] +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/ModProg/manyhow" +documentation = "https://docs.rs/manyhow" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[workspace] +members = [ "macros", ".", "examples/macro", "examples/no_macro" ] + +[dependencies] +macros = { package = "manyhow-macros", path = "macros", version = "0.11.4", optional = true} +proc-macro2 = "1.0.60" +quote = "1" +syn1 = { package = "syn", version = "1", default-features = false, optional = true, features = ["printing"] } +syn2 = { package = "syn", version = "2", default-features = false, optional = true, features = ["printing", "parsing"] } +darling_core = { version = "0.20.1", optional = true } + +[features] +default = ["syn", "macros"] +syn = ["syn2"] +darling = ["darling_core"] + +[dev-dependencies] +proc-macro-utils = "0.8.0" +proc-macro2 = { version = "1", features = ["span-locations"] } +syn2 = {package = "syn", version = "2", features = ["full"]} + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.release] +shared-version = true + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = '## \[Unreleased\]' +replace = """ + +## [{{version}}] - {{date}}\ +""" +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = '\[unreleased\]: (.*)/(v.*)\.\.\.HEAD' +replace = """ +[unreleased]: $1/{{tag_name}}...HEAD +[{{version}}]: $1/$2...{{tag_name}}\ +""" diff --git a/third-party/manyhow/LICENSE-APACHE b/third-party/manyhow/LICENSE-APACHE new file mode 100644 index 0000000..da3ea28 --- /dev/null +++ b/third-party/manyhow/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a peretual, + worldwide, non-exclusive, no-cpharge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides it s Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third-party/manyhow/LICENSE-MIT b/third-party/manyhow/LICENSE-MIT new file mode 100644 index 0000000..7c5aea4 --- /dev/null +++ b/third-party/manyhow/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Roland Fredenhagen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third-party/manyhow/README.md b/third-party/manyhow/README.md new file mode 100644 index 0000000..e9a257b --- /dev/null +++ b/third-party/manyhow/README.md @@ -0,0 +1,122 @@ +# manyhow +## anyhow for proc macros +[![CI Status](https://github.com/ModProg/manyhow/actions/workflows/test.yaml/badge.svg)](https://github.com/ModProg/manyhow/actions/workflows/test.yaml) +[![Crates.io](https://img.shields.io/crates/v/manyhow)](https://crates.io/crates/manyhow) +[![Docs.rs](https://img.shields.io/crates/v/template?color=informational&label=docs.rs)](https://docs.rs/manyhow) +[![Documentation for `main`](https://img.shields.io/badge/docs-main-informational)](https://modprog.github.io/manyhow/manyhow/) + +Proc **m**acro **anyhow**, a combination of ideas from +[`anyhow`](https://docs.rs/anyhow) and +[`proc-macro-error`](https://docs.rs/proc-macro-error) to improve proc macro +development, especially focused on the error handling. + +## Motivation +Error handling in proc-macros is unideal, as the top level functions of proc +macros can only return `TokenStreams` both in success and failure case. This +means that I often write code like this, moving the actual implementation in +a separate function to be able to use the ergonomic rust error handling with +e.g., `?`. +```rust +use proc_macro2::TokenStream as TokenStream2; + +#[proc_macro] +pub fn my_macro(input: TokenStream) -> TokenStream { + match actual_implementation(input.into()) { + Ok(output) => output, + Err(error) => error.into_compile_error(), + } + .into() +} + +fn actual_implementation(input: TokenStream2) -> syn::Result { + // .. +} +``` + +## Using the `#[manyhow]` macro +To activate the error handling, just add `#[manyhow]` above any +proc macro implementation, reducing the above example to: + +```rust +use manyhow::manyhow; +use proc_macro2::TokenStream as TokenStream2; + +#[manyhow] +#[proc_macro] +fn my_macro(input: TokenStream2) -> syn::Result { + // .. +} +``` + +See [Without macros](#without-macros) to see what this expands to under the +hood. + +A proc macro function marked as `#[manyhow]` can take and return any +`TokenStream` and can also return `Result` where `E` implements `ToTokensError`. As additional parameters a +[dummy](#dummy-mut-tokenstream) and/or [emitter](#emitter-mut-emitter) can +be specified. + +The `manyhow` attribute takes one optional flag when used for `proc_macro` +and `proc_macro_attribute`. `#[manyhow(input_as_dummy)]` will take the input +of a function like `proc_macro` to initialize the [dummy `&mut +TokenStream`](#dummy-mut-tokenstream) while `#[manyhow(item_as_dummy)]` on +`proc_macro_attribute` will initialize the dummy with the annotated item. + +## Without macros +`manyhow` can be used without proc macros, and they can be disabled by +adding `manyhow` with `default-features=false`. + +The usage is more or less the same, though with some added boilerplate from +needing to invoke one of `function`, `attribute` or `derive` +directly. + +While the examples use closures, functions can be passed in as well. The +above example would then change to: +```rust +use proc_macro2::TokenStream as TokenStream2; + +#[proc_macro] +pub fn my_macro(input: TokenStream) -> TokenStream { + manyhow::function( + input, + false, + |input: TokenStream2| -> syn::Result { + // .. + }, + ) +} +``` +[`Emitter`](#emitter-mut-emitter) and [dummy +`TokenStream`](#dummy-mut-tokenstream) can also be used. `function` and +`attribute` take an additional boolean parameter controlling whether the +input/item will be used as initial dummy. + +## `emitter: &mut Emitter` +`MacroHandler`s (the trait defining what closures/functions can be used +with `manyhow`) can take a mutable reference to an `Emitter`. This +allows to collect errors, but not fail immediately. + +`Emitter::into_result` can be used to return if an `Emitter` contains +any values. + +```rust +use manyhow::{manyhow, Emitter, ErrorMessage}; +use proc_macro2::TokenStream as TokenStream2; + +#[manyhow] +#[proc_macro] +fn my_macro(input: TokenStream2, emitter: &mut Emitter) -> manyhow::Result { + // .. + emitter.emit(ErrorMessage::call_site("A fun error!")); + emitter.into_result()?; + // .. +} +``` + +## `dummy: &mut TokenStream` +`MacroHandler`s also take a mutable reference to a `TokenStream`, to +enable emitting some dummy code to be used in case the macro errors. + +This allows either appending tokens e.g., with `ToTokens::to_tokens` or +directly setting the dummy code e.g., `*dummy = quote!{some tokens}`. diff --git a/third-party/manyhow/rustfmt.toml b/third-party/manyhow/rustfmt.toml new file mode 100644 index 0000000..c7cf2dc --- /dev/null +++ b/third-party/manyhow/rustfmt.toml @@ -0,0 +1,15 @@ +condense_wildcard_suffixes = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +hex_literal_case = "Upper" +imports_granularity = "Module" +normalize_comments = true +normalize_doc_attributes = true +overflow_delimited_expr = true +reorder_impl_items = true +group_imports = "StdExternalCrate" +unstable_features = true +use_field_init_shorthand = true +version = "Two" +wrap_comments = true diff --git a/third-party/manyhow/src/error.rs b/third-party/manyhow/src/error.rs new file mode 100644 index 0000000..0350682 --- /dev/null +++ b/third-party/manyhow/src/error.rs @@ -0,0 +1,515 @@ +#![allow(clippy::missing_errors_doc)] +use std::convert::Infallible; +use std::fmt::{Debug, Display}; +use std::mem; +use std::ops::{Add, AddAssign, Range}; + +#[cfg(feature = "darling")] +use darling_core::Error as DarlingError; +use proc_macro2::{Span, TokenStream}; +use quote::{quote_spanned, ToTokens}; +#[cfg(feature = "syn1")] +use syn1::Error as Syn1Error; +#[cfg(feature = "syn2")] +use syn2::Error as Syn2Error; + +#[cfg(doc)] +use crate::MacroOutput; +use crate::{to_tokens_span_range, SpanRanged}; + +/// An alias for [`Result`](std::result::Result) suited for use with this crate +pub type Result = std::result::Result; + +/// Error that does not expand to any [`compile_error!`] and therefor does not +/// cause compilation to fail. +#[derive(Debug)] +pub struct SilentError; + +/// This crates Error type +#[derive(Debug)] +#[must_use] +pub struct Error(Vec>); +#[cfg(feature = "syn1")] +impl From for Error { + fn from(error: Syn1Error) -> Self { + Self::from(error) + } +} +#[cfg(feature = "syn2")] +impl From for Error { + fn from(error: Syn2Error) -> Self { + Self::from(error) + } +} +#[cfg(feature = "darling")] +impl From for Error { + fn from(error: DarlingError) -> Self { + Self::from(error) + } +} +impl From for Error { + fn from(error: ErrorMessage) -> Self { + Self::from(error) + } +} +impl From for Error { + fn from(_: SilentError) -> Self { + Self(Vec::new()) + } +} + +impl Add for Error { + type Output = Error; + + fn add(self, rhs: T) -> Self::Output { + self.join(rhs) + } +} + +impl AddAssign for Error { + fn add_assign(&mut self, rhs: T) { + self.push(rhs); + } +} + +impl Error { + /// Mimics [`From for Error`](From) implementation to + /// not conflict std's `From for T` + pub fn from(error: impl ToTokensError + 'static) -> Self { + Self(vec![Box::new(error)]) + } + + /// Pushes an additional `Error` + /// + /// Alternatively errors can also be "added": + /// + /// ``` + /// use manyhow::{Error, error_message}; + /// let mut error = Error::from(error_message!("Hello Rust!")); + /// error += error_message!("Hello 🦀!"); + /// # use manyhow::ToTokensError; + /// # proc_macro_utils::assert_tokens!(error.into_token_stream(), + /// # { ::core::compile_error!{"Hello Rust!"} ::core::compile_error!{"Hello 🦀!"} }); + /// ``` + pub fn push(&mut self, error: impl ToTokensError + 'static) { + self.0.push(Box::new(error)); + } +} + +impl Extend for Error { + fn extend>(&mut self, iter: T) { + self.0.extend( + iter.into_iter() + .map(|i| Box::new(i) as Box), + ); + } +} + +/// A single error message +/// +/// Can take additional attachments like [`help`](Self::help) or +/// [`note`](Self::note). +/// +/// Implements `ToTokensError` and can therefore be used with +/// [`MacroOutput`]s. +#[derive(Debug)] +#[must_use] +pub struct ErrorMessage { + span: Range, + msg: String, + attachments: Vec<(&'static str, String)>, +} +impl Display for ErrorMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.msg.trim_end())?; + if !self.attachments.is_empty() { + write!(f, "\n\n")?; + } + for (label, attachment) in &self.attachments { + let mut attachment = attachment.lines(); + writeln!( + f, + " = {label}: {}", + attachment.next().expect("should return at least one line") + )?; + for line in attachment { + // `labels` should always be one char per cell + writeln!(f, " {1:2$} {}", line, "", label.len())?; + } + } + Ok(()) + } +} +impl ToTokensError for ErrorMessage { + fn to_tokens(&self, tokens: &mut TokenStream) { + let msg = self.to_string(); + let msg = quote_spanned!(self.span.end => {#msg}); + quote_spanned! {self.span.start => + ::core::compile_error! #msg + } + .to_tokens(tokens); + } +} + +#[cfg(feature = "syn1")] +impl From for Syn1Error { + fn from(value: ErrorMessage) -> Self { + Self::new_spanned(value.to_token_stream(), value) + } +} +#[cfg(feature = "syn2")] +impl From for Syn2Error { + fn from(value: ErrorMessage) -> Self { + Self::new_spanned(value.to_token_stream(), value) + } +} + +impl Add for ErrorMessage { + type Output = Error; + + fn add(self, rhs: T) -> Self::Output { + self.join(rhs) + } +} + +impl ErrorMessage { + /// Creates a new error message at the specified span + /// + /// This function takes a [`SpanRanged`] meaning you can also pass a + /// [`Range`](Range)[``](Span) (i.e. `first..last`) for better error + /// messages on multi token values, for details see + /// [SpanRanged#motivation](SpanRanged#motivation) + /// + /// If your type implements [`ToTokens`] use [`ErrorMessage::spanned`] + /// instead. + pub fn new(span: impl SpanRanged, msg: impl Display) -> Self { + Self { + span: span.span_range(), + msg: msg.to_string(), + attachments: Vec::new(), + } + } + + /// Creates an error message pointing to the complete token stream `tokens` + /// expands to + pub fn spanned(tokens: impl ToTokens, msg: impl Display) -> Self { + Self { + span: to_tokens_span_range(tokens), + msg: msg.to_string(), + attachments: Vec::new(), + } + } + + /// Creates a new error message at [`Span::call_site`] prefer + /// [`ErrorMessage::new`] or [`ErrorMessage::spanned`] with the correct span + /// for a more helpful output. + pub fn call_site(msg: impl Display) -> Self { + Self::new(Span::call_site(), msg) + } + + /// Attaches an additional message to `self` reusing the same + /// span, and the specified `label`. + pub fn attachment(mut self, label: &'static str, msg: impl Display) -> Self { + self.attachments.push((label, msg.to_string())); + self + } + + /// Attaches a new `error` message to `self` reusing the same span + pub fn error(self, msg: impl Display) -> Self { + self.attachment("error", msg) + } + + /// Attaches a new `warning` message to `self` reusing the same span + pub fn warning(self, msg: impl Display) -> Self { + self.attachment("warning", msg) + } + + /// Attaches a new `note` message to `self` reusing the same span + pub fn note(self, msg: impl Display) -> Self { + self.attachment("note", msg) + } + + /// Attaches a new `help` message to `self` reusing the same span + pub fn help(self, msg: impl Display) -> Self { + self.attachment("help", msg) + } +} + +/// Exposes [`ErrorMessage::attachment`] as a trait to allow +/// [`ResultExt::attachment`]. +pub trait Attachment: Sized { + /// Attaches an additional message to `self` reusing the same + /// span, and the specified `label`. + #[must_use] + fn attachment(self, label: &'static str, msg: impl Display) -> Self; +} + +impl Attachment for ErrorMessage { + fn attachment(mut self, label: &'static str, msg: impl Display) -> Self { + self.attachments.push((label, msg.to_string())); + self + } +} + +/// Allows emitting errors without returning. +#[derive(Default, Debug)] +pub struct Emitter(Vec>); + +impl AddAssign for Emitter { + fn add_assign(&mut self, rhs: T) { + self.emit(rhs); + } +} + +impl Emitter { + /// Creates an `Emitter`, this can be used to collect errors than can later + /// be converted with [`Emitter::into_result()`]. + #[must_use] + pub fn new() -> Self { + Emitter(Vec::new()) + } + + pub(crate) fn to_tokens(&self, tokens: &mut TokenStream) { + for error in &self.0 { + error.to_tokens(tokens); + } + } + + /// Emits an error + /// + /// Alternatively errors can also be "added": + /// + /// ``` + /// let mut emitter = manyhow::Emitter::new(); + /// emitter += manyhow::error_message!("Hello World!"); + /// # use manyhow::ToTokensError; + /// # proc_macro_utils::assert_tokens!(emitter.into_result().unwrap_err().into_token_stream(), { ::core::compile_error!{"Hello World!"} }); + /// ``` + pub fn emit(&mut self, error: impl ToTokensError + 'static) { + self.0.push(Box::new(error)); + } + + /// Checks if any errors were emitted + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Removes all emitted errors + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Returns emitted errors if not [`Self::is_empty`]. + /// + /// If no errors where emitted, returns `Ok(())`. + /// + /// *Note:* This clears the emitter to avoid returning duplicate errors. + pub fn into_result(&mut self) -> Result<(), Error> { + if self.is_empty() { + Ok(()) + } else { + Err(Error(mem::take(&mut self.0))) + } + } +} + +impl Extend for Emitter { + fn extend>(&mut self, iter: T) { + self.0.extend( + iter.into_iter() + .map(|i| Box::new(i) as Box), + ); + } +} + +/// Error that can be converted to a [`TokenStream`] required to be used with +/// [`MacroOutput`] +/// +/// This trait is equivalent to [`ToTokens`]. +pub trait ToTokensError: Debug { + /// Equivalent to [`ToTokens::to_tokens`] + fn to_tokens(&self, tokens: &mut TokenStream); + /// Equivalent to [`ToTokens::to_token_stream`] + fn to_token_stream(&self) -> TokenStream { + let mut tokens = TokenStream::new(); + self.to_tokens(&mut tokens); + tokens + } + /// Equivalent to [`ToTokens::into_token_stream`] + fn into_token_stream(self) -> TokenStream + where + Self: Sized, + { + self.to_token_stream() + } +} + +/// Allows to call `.join(..)` on any `impl ToTokensError` +pub trait JoinToTokensError { + /// Joins two `Error`s + /// + /// ``` + /// use manyhow::error_message; + /// # use crate::manyhow::JoinToTokensError; + /// + /// error_message!("test").join(error_message!("another")); + /// ``` + /// + /// Some errors like [`manyhow::Error`] and [`manyhow::ErrorMessage`] can + /// also be "added": + /// + /// ``` + /// # use manyhow::ToTokensError; + /// use manyhow::{error_message, Error}; + /// # proc_macro_utils::assert_tokens!(( + /// error_message!("Hello Rust!") + error_message!("Hello 🦀!") + /// # ).into_token_stream(), { ::core::compile_error!{"Hello Rust!"} ::core::compile_error!{"Hello 🦀!"} }); + /// # proc_macro_utils::assert_tokens!(( + /// Error::from(error_message!("Hello Rust!")) + error_message!("Hello 🦀!") + /// # ).into_token_stream(), { ::core::compile_error!{"Hello Rust!"} ::core::compile_error!{"Hello 🦀!"} }); + /// ``` + fn join(self, error: impl ToTokensError + 'static) -> Error; +} + +impl JoinToTokensError for T { + fn join(self, error: impl ToTokensError + 'static) -> Error { + let mut this = Error::from(self); + this.push(error); + this + } +} + +impl ToTokensError for Infallible { + fn to_tokens(&self, _: &mut TokenStream) { + unreachable!() + } +} +#[cfg(feature = "syn1")] +impl ToTokensError for Syn1Error { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.to_compile_error().to_tokens(tokens); + } +} +#[cfg(feature = "syn")] +impl ToTokensError for Syn2Error { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.to_compile_error().to_tokens(tokens); + } +} +#[cfg(feature = "darling")] +impl ToTokensError for DarlingError { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.clone().write_errors().to_tokens(tokens); + } +} +impl ToTokensError for Error { + fn to_tokens(&self, tokens: &mut TokenStream) { + for error in &self.0 { + error.to_tokens(tokens); + } + } +} +impl ToTokensError for SilentError { + fn to_tokens(&self, _: &mut TokenStream) {} +} + +/// Some utilities on [`Result`](ToTokensError) +pub trait ResultExt: Sized { + /// If self is error, attaches another error + fn context(self, error: impl ToTokensError + 'static) -> Result { + self.context_with(|| error) + } + + /// If self is error, attaches another error, closure is only executed if + /// the `Result` is `Err` + fn context_with( + self, + error: impl FnOnce() -> C, + ) -> Result; + /// If self is error, extend error message + /// + /// Only works if `E` implements [`Attachment`] which is the case for + /// [`ErrorMessage`] + #[must_use] + fn attachment(self, label: &'static str, msg: impl Display) -> Self + where + E: Attachment; + + /// Attaches a new `error` message to `self` reusing the same span + #[must_use] + fn error(self, msg: impl Display) -> Self + where + E: Attachment, + { + self.attachment("error", msg) + } + + /// Attaches a new `warning` message to `self` reusing the same span + #[must_use] + fn warning(self, msg: impl Display) -> Self + where + E: Attachment, + { + self.attachment("warning", msg) + } + + /// Attaches a new `note` message to `self` reusing the same span + #[must_use] + fn note(self, msg: impl Display) -> Self + where + E: Attachment, + { + self.attachment("note", msg) + } + + /// Attaches a new `help` message to `self` reusing the same span + #[must_use] + fn help(self, msg: impl Display) -> Self + where + E: Attachment, + { + self.attachment("help", msg) + } +} + +impl ResultExt for Result { + fn context_with( + self, + error: impl FnOnce() -> C, + ) -> Result { + self.map_err(|e| { + let mut e = Error::from(e); + e.push(error()); + e + }) + } + + fn attachment(self, label: &'static str, msg: impl Display) -> Result + where + E: Attachment, + { + self.map_err(|e| e.attachment(label, msg)) + } +} + +#[cfg(test)] +mod test { + use proc_macro_utils::assert_tokens; + + use super::*; + + #[test] + fn error_message() { + let error_message = ErrorMessage::new(Span::call_site(), "test message") + .help("try to call your dog") + .note("you could use the banana phone") + .warning("be careful") + .error("you cannot reach them"); + assert_tokens! {error_message.to_token_stream(), { + ::core::compile_error! { + "test message\n\n = help: try to call your dog\n = note: you could use the banana phone\n = warning: be careful\n = error: you cannot reach them\n" + } + }} + } +} diff --git a/third-party/manyhow/src/lib.rs b/third-party/manyhow/src/lib.rs new file mode 100644 index 0000000..1a66edf --- /dev/null +++ b/third-party/manyhow/src/lib.rs @@ -0,0 +1,850 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![warn(clippy::pedantic, missing_docs)] +#![allow(clippy::module_name_repetitions)] +//! Proc **m**acro **anyhow**, a combination of ideas from +//! [`anyhow`](docs.rs/anyhow) and +//! [`proc-macro-error`](docs.rs/proc-macro-error) to improve proc macro +//! development, especially focused on the error handling. +//! +//! # Motivation +//! Error handling in proc-macros is unideal, as the top level functions of +//! proc-macros can only return `TokenStreams` both in success and failure case. +//! This means that I often write code like this, moving the actual +//! implementation in a separate function to be able to use the ergonomic rust +//! error handling with e.g., `?`. +//! ``` +//! # use proc_macro2::TokenStream; +//! # use quote::quote; +//! # use syn2 as syn; +//! use proc_macro2::TokenStream as TokenStream2; +//! +//! # let _ = quote!{ +//! #[proc_macro] +//! # }; +//! pub fn my_macro(input: TokenStream) -> TokenStream { +//! match actual_implementation(input.into()) { +//! Ok(output) => output, +//! Err(error) => error.into_compile_error(), +//! } +//! .into() +//! } +//! +//! fn actual_implementation(input: TokenStream2) -> syn::Result { +//! // .. +//! # Ok(quote!()) +//! } +//! ``` +//! +//! # Using the `#[manyhow]` macro +//! To activate the error handling, just add [`#[manyhow]`](manyhow) above any +//! proc-macro implementation, reducing the above example to: +//! +//! ``` +//! # use quote::quote; +//! # use syn2 as syn; +//! use manyhow::manyhow; +//! use proc_macro2::TokenStream as TokenStream2; +//! +//! # let _ = quote!{ +//! #[manyhow] +//! #[proc_macro] +//! # }; +//! // You can also merge the two attributes: #[manyhow(proc_macro)] +//! fn my_macro(input: TokenStream2) -> syn::Result { +//! // .. +//! # Ok(quote!()) +//! } +//! +//! // On top of the TokenStreams any type that implements [`Parse`] is supported +//! # let _ = quote!{ +//! #[manyhow(proc_macro_derive(MyMacro))] +//! #[proc_macro] +//! # }; +//! // The output can also be anything that implements `quote::ToTokens` +//! fn my_derive_macro(input: syn::DeriveInput) -> manyhow::Result { +//! // .. +//! # manyhow::bail!("hello") +//! } +//! ``` +//! +//! See [Without macros](#without-macros) to see what this expands to under the +//! hood. +//! +//! You can also use the `#[manyhow]` attributes on a use statement, useful when +//! moving your proc-macro implementations in separate modules. +//! +//! ``` +//! # use quote::quote; +//! use manyhow::manyhow; +//! +//! mod module { +//! # use quote::quote; +//! # use syn2 as syn; +//! use proc_macro2::TokenStream as TokenStream2; +//! +//! pub fn my_macro(input: TokenStream2) -> syn::Result { +//! // .. +//! # Ok(quote!()) +//! } +//! } +//! +//! # let _ = quote!{ +//! #[manyhow] +//! #[proc_macro] +//! # }; +//! // You can also merge the two attributes: #[manyhow(proc_macro)] +//! pub use module::my_macro; +//! ``` +//! +//! A proc macro function marked as `#[manyhow]` can take and return any +//! [`TokenStream`](AnyTokenStream), and can also return `Result` where `E` implements [`ToTokensError`]. As additional parameters a +//! [dummy](#dummy-mut-tokenstream) and/or [emitter](#emitter-mut-emitter) can +//! be specified. +//! +//! The `manyhow` attribute takes optional flags to configure its behavior. +//! +//! When used for `proc_macro` and `proc_macro_attribute`, +//! `#[manyhow(input_as_dummy, ...)]` will take the input of a function like +//! `proc_macro` to initialize the [dummy `&mut TokenStream`](# +//! dummy-mut-tokenstream) while `#[manyhow(item_as_dummy, ...)]` on +//! `proc_macro_attribute` will initialize the dummy with the annotated item. +//! +//! You can merge the `#[proc_macro*]` attribute inside the manyhow flags e.g., +//! `#[manyhow(proc_macro)]` or `#[manyhow(proc_macro_derive(SomeTrait, ...))]`. +//! +//! The `#[manyhow(impl_fn, ...)]` flag will put the actual macro implementation +//! in a separate function. Making it available for e.g., unit testing with +//! [`proc_macro_utils::assert_expansion!`](https://docs.rs/proc-macro-utils/latest/proc_macro_utils/macro.assert_expansion.html). +//! +//! ```ignore +//! #[manyhow(impl_fn)] +//! #[proc_macro] +//! pub fn actual_macro(input: TokenStream2) -> TokenStream2 { +//! // ... +//! } +//! // would roughly expand to +//! #[proc_macro] +//! pub fn actual_macro(input: TokenStream) -> TokenStream { +//! actual_macro_impl(input.into()).into() +//! } +//! fn actual_macro_impl(input: TokenStream2) -> TokenStream2 { +//! // ... +//! } +//! ``` +//! +//! # Without macros +//! `manyhow` can be used without proc macros, and they can be disabled by +//! adding `manyhow` with `default-features=false`. +//! +//! The usage is more or less the same, though with some added boilerplate from +//! needing to invoke one of [`function()`] ([`function!`]), [`attribute()`] +//! ([`attribute!`]) or [`derive()`] ([`derive!`]) directly. For each version +//! there exists a function and a `macro_rules` macro, while the function only +//! supports [`proc_macro::TokenStream`] and [`proc_macro2::TokenStream`], the +//! macro versions also support any type that implements [`Parse`] +//! and [`ToTokens`] respectively. +//! +//! While the examples use closures, functions can be passed in as well. The +//! above example would then change to: +//! ``` +//! # use proc_macro2::TokenStream; +//! # use quote::quote; +//! # use syn2 as syn; +//! use proc_macro2::TokenStream as TokenStream2; +//! +//! # let _ = quote!{ +//! #[proc_macro] +//! # }; +//! pub fn my_macro(input: TokenStream) -> TokenStream { +//! # let tmp = input.clone(); +//! # let output: TokenStream = +//! manyhow::function( +//! input, +//! false, +//! |input: TokenStream2| -> syn::Result { +//! // .. +//! # Ok(quote!()) +//! }, +//! ) +//! # ; +//! # let input = tmp; +//! // Or +//! manyhow::function!( +//! input, +//! |input: syn::DeriveInput| -> manyhow::Result { +//! // .. +//! # manyhow::bail!("error") +//! }, +//! ) +//! } +//! ``` +//! [`Emitter`](#emitter-mut-emitter) and [dummy +//! `TokenStream`](#dummy-mut-tokenstream) can also be used. [`function()`] +//! ([`function!`]) and [`attribute()`] ([`attribute!`]) take an additional +//! boolean parameter controlling whether the input/item will be used as initial +//! dummy. +//! +//! # `emitter: &mut Emitter` +//! [`*MacroHandler`](FunctionMacroHandler)s (the traits defining what +//! closures/functions can be used with `manyhow`) can take a mutable reference +//! to an [`Emitter`]. This allows collecting errors, but not fail immediately. +//! +//! [`Emitter::into_result`] can be used to return if an [`Emitter`] contains +//! any values. +//! +//! ``` +//! # use quote::quote; +//! # use syn2 as syn; +//! use manyhow::{manyhow, Emitter, ErrorMessage}; +//! use proc_macro2::TokenStream as TokenStream2; +//! +//! # let _ = quote!{ +//! #[manyhow] +//! #[proc_macro] +//! # }; +//! fn my_macro(input: TokenStream2, emitter: &mut Emitter) -> manyhow::Result { +//! // .. +//! emitter.emit(ErrorMessage::call_site("A fun error!")); +//! emitter.into_result()?; +//! // .. +//! # Ok(quote!()) +//! } +//! ``` +//! +//! # `dummy: &mut TokenStream` +//! [`*MacroHandler`](FunctionMacroHandler)s can also take a mutable reference +//! to a `TokenStream`, to enable emitting some dummy code to be used in case +//! the macro errors. +//! +//! This allows either appending tokens e.g., with [`ToTokens::to_tokens`] or +//! directly setting the dummy code e.g., `*dummy = quote!{some tokens}`. +//! +//! # Crate features +//! +//! - `macros` **default** Enables [`#[manyhow]`](macros::manyhow) attribute +//! macro. +//! - `syn`/`syn2` **default** Enables errors for [`syn` 2.x](https://docs.rs/syn/latest/syn/). +//! - `syn1` Enables errors for [`syn` 1.x](https://docs.rs/syn/1.0.109/syn/index.html). +//! - `darling` Enables errors for [`darling`](https://docs.rs/darling/latest/index.html). + +#[cfg(feature = "macros")] +pub use macros::manyhow; +use proc_macro2::TokenStream; +#[cfg(doc)] +use {quote::ToTokens, syn2::parse::Parse}; + +extern crate proc_macro; + +#[macro_use] +mod span_ranged; +pub use span_ranged::{to_tokens_span_range, SpanRanged}; +#[macro_use] +mod macro_rules; +mod error; +pub use error::*; + +mod parse_to_tokens; + +#[doc(hidden)] +pub mod __private { + pub use std::prelude::rust_2021::*; + + use proc_macro2::TokenStream; + pub use quote; + + pub use crate::span_ranged::*; + pub type Dummy = Option; + + pub use crate::parse_to_tokens::*; +} + +/// Marker trait for [`proc_macro::TokenStream`] and +/// [`proc_macro2::TokenStream`] +pub trait AnyTokenStream: Clone + From + Into + Default {} +impl AnyTokenStream for TokenStream {} +impl AnyTokenStream for proc_macro::TokenStream {} + +#[macro_export] +#[doc(hidden)] +macro_rules! __macro_handler { + ($name:ident; $($(#attr=$attr:tt)? $n:ident: $input:expr),+; $impl:expr$(; dummy:$dummy:expr)?) => { + $crate::__macro_handler! {! $name; $($(#attr=$attr)? $n: $input.clone()),+; $impl $(; $crate::__private::Some($dummy))?} + }; + ($name:ident; $($(#attr=$attr:tt)? $n:ident: $input:expr),+; $impl:expr; dummy) => { + $crate::__macro_handler! {! $name; $($(#attr=$attr)? $n: $input),+; $impl; $crate::__private::Dummy::None} + }; + (! $name:ident; $($(#attr=$attr:tt)? $n:ident: $input:expr),+; $impl:expr $(; $dummy:expr)?) => {{ + use $crate::__private::{ManyhowParse, ManyhowToTokens, ManyhowTry}; + let implementation = $impl; + $(let $n = &$crate::__private::WhatType::new();)+ + if false { + _ = $crate::__private::$name($($n.identify(),)+ $($dummy,)? implementation); + unreachable!(); + } else { + match $crate::__private::$name($( + {#[allow(unused)] + let attr = false; + $(let attr = $attr;)? + $n.manyhow_parse($input, attr)}, + )+ $($dummy,)? implementation) + { + Err(tokens) => tokens.into(), + Ok((output, mut tokens, mut dummy)) => { + match (&$crate::__private::WhatType::from(&output)).manyhow_try(output) { + Err(error) => { + dummy.extend(tokens); + tokens = dummy; + (&$crate::__private::WhatType::from(&error)).manyhow_to_tokens(error, &mut tokens); + }, + Ok(output) => (&$crate::__private::WhatType::from(&output)).manyhow_to_tokens(output, &mut tokens), + }; + tokens.into() + } + } + } + }}; +} + +/// Handles [`proc_macro_attribute`](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) +/// implementation +/// +/// Takes any `TokenStream` for `input` and `item` and returns any +/// `TokenStream`. If `item_as_dummy = true` the item input will be used as +/// default dummy code on error. `body` takes a [`AttributeMacroHandler`] with +/// two `TokenStream` parameters. And an optional [`&mut Emitter`](Emitter) and +/// a `&mut TokenStream` for storing a dummy output. +/// +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// # use quote::{quote, ToTokens}; +/// use manyhow::{attribute, Emitter, Result}; +/// use proc_macro2::TokenStream; +/// # let input = quote!(); +/// # let item = quote!(); +/// # let output: TokenStream = +/// attribute( +/// input, +/// item, +/// false, +/// |input: TokenStream, +/// item: TokenStream, +/// dummy: &mut TokenStream, +/// emitter: &mut Emitter| +/// -> Result { +/// // .. +/// # Ok(quote!()) +/// }, +/// ); +/// ``` +/// +/// *Note:* When `item_as_dummy = true` the `dummy: &mut TokenStream` will be +/// initialized with `item`. To override assign a new `TokenStream`: +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// use manyhow::{attribute, Result, SilentError}; +/// use proc_macro2::TokenStream; +/// use quote::{quote, ToTokens}; +/// # let input = quote!(input); +/// let item = quote!( +/// struct Struct; +/// ); +/// let output: TokenStream = attribute( +/// input, +/// item, +/// true, +/// |input: TokenStream, +/// item: TokenStream, +/// dummy: &mut TokenStream| +/// -> Result { +/// assert_tokens!(dummy.to_token_stream(), { +/// struct Struct; +/// }); +/// *dummy = quote! { +/// struct Struct(HelloWorld); +/// }; +/// // .. +/// Err(SilentError) +/// }, +/// ); +/// +/// assert_tokens! {output, {struct Struct(HelloWorld);}}; +/// ``` +pub fn attribute< + Input: AnyTokenStream, + Item: AnyTokenStream, + Dummy: AnyTokenStream, + Output: MacroOutput, + Return: AnyTokenStream, + Function, +>( + input: impl AnyTokenStream, + item: impl AnyTokenStream, + item_as_dummy: bool, + body: impl AttributeMacroHandler< + Function, + Item = Item, + Input = Input, + Dummy = Dummy, + Output = Output, + >, +) -> Return { + #[allow(unused_mut)] + let mut tokens = Dummy::default(); + let mut tokens = if item_as_dummy { + item.clone().into().into() + } else { + tokens + }; + let mut emitter = Emitter::new(); + let output = body.call( + input.into().into(), + item.into().into(), + &mut tokens, + &mut emitter, + ); + let mut tokens = tokens.into(); + let mut tokens = match output.convert() { + Ok(tokens) => tokens, + Err(error) => { + error.to_tokens(&mut tokens); + tokens + } + }; + emitter.to_tokens(&mut tokens); + tokens.into() +} + +/// Handles [`proc_macro_attribute`](https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros) +/// implementation +/// +/// Takes any `TokenStream` for `input` and `item` and its return value. If +/// `#[as_dummy]` is specified on item, it will be used as default +/// dummy code on error. `body` takes a [`AttributeMacroHandler`] with two +/// `TokenStream`s or types implementing [`Parse`] parameters and returning a +/// `TokenStream` or type implementing [`ToTokens`]. And an optional [`&mut +/// Emitter`](Emitter) and a `&mut TokenStream` for storing a dummy output. +/// +/// +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// # use quote::{quote, ToTokens}; +/// use manyhow::{attribute, Emitter, Result}; +/// use proc_macro2::TokenStream; +/// # let input = quote!(); +/// # let item = quote!(); +/// # let output: TokenStream = +/// attribute!(input, item, |input: TokenStream, +/// item: TokenStream, +/// dummy: &mut TokenStream, +/// emitter: &mut Emitter| +/// -> Result { +/// // .. +/// # Ok(quote!()) +/// }); +/// ``` +/// +/// *Note:* When `#[as_dummy]` is specified the `dummy: &mut TokenStream` will +/// be initialized with `item`. To override assign a new `TokenStream`: +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// # use syn2 as syn; +/// use manyhow::{attribute, Result, SilentError}; +/// use proc_macro2::TokenStream; +/// use quote::{quote, ToTokens}; +/// # let input = quote!(input); +/// let item = quote!( +/// struct Struct; +/// ); +/// let output: TokenStream = attribute!( +/// input, +/// #[as_dummy] +/// item, +/// |input: TokenStream, +/// item: syn::ItemStruct, +/// dummy: &mut TokenStream| +/// -> Result { +/// assert_tokens!(dummy.to_token_stream(), { +/// struct Struct; +/// }); +/// *dummy = quote! { +/// struct Struct(HelloWorld); +/// }; +/// // .. +/// Err(SilentError) +/// }, +/// ); +/// +/// assert_tokens! {output, {struct Struct(HelloWorld);}}; +/// ``` +#[macro_export] +macro_rules! attribute { + ($input:expr, #[as_dummy] $item:expr, $impl:expr $(,)?) => { + $crate::__macro_handler!{attribute_transparent; #attr=true input: $input, item: $item.clone(); $impl; dummy: $item} + }; + ($input:expr, $item:expr, $impl:expr $(,)?) => { + $crate::__macro_handler!{attribute_transparent; #attr=true input: $input, item: $item; $impl; dummy} + }; +} + +/// Handles [`proc_macro_derive`](https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros) +/// implementation. +/// +/// Use [`derive!`] to support [`Parse`] and [`ToTokens`] as well. +/// +/// Takes any `TokenStream` for `item` and returns any `TokenStream`. `body` +/// takes a [`DeriveMacroHandler`] with one `TokenStream` parameter. And an +/// optional [`&mut Emitter`](Emitter) and `&mut TokenStream` for storing a +/// dummy output. +/// +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// # use quote::{quote, ToTokens}; +/// use manyhow::{derive, Emitter, Result}; +/// use proc_macro2::TokenStream; +/// # let item = quote!(); +/// # let output: TokenStream = +/// derive( +/// item, +/// |item: TokenStream, dummy: &mut TokenStream, emitter: &mut Emitter| -> Result { +/// // .. +/// # Ok(quote!()) +/// }, +/// ); +/// ``` +pub fn derive< + Item: AnyTokenStream, + Dummy: AnyTokenStream, + Output: MacroOutput, + Return: AnyTokenStream, + Function, +>( + item: impl AnyTokenStream, + body: impl DeriveMacroHandler, +) -> Return { + let mut tokens = Dummy::default(); + let mut emitter = Emitter::new(); + let output = body.call(item.into().into(), &mut tokens, &mut emitter); + let mut tokens = tokens.into(); + let mut tokens = match output.convert() { + Ok(tokens) => tokens, + Err(error) => { + error.to_tokens(&mut tokens); + tokens + } + }; + emitter.to_tokens(&mut tokens); + tokens.into() +} + +/// Handles [`proc_macro_derive`](https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros) +/// implementation. +/// +/// Takes any `TokenStream` for `item` and returns any `TokenStream`. `body` +/// takes a [`DeriveMacroHandler`] with one `TokenStream` or type implementing +/// [`Parse`] parameter and returns a `TokenStream` or type implementing +/// [`ToTokens`]. And an optional [`&mut Emitter`](Emitter) and `&mut +/// TokenStream` for storing a dummy output. +/// +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// # use quote::{quote, ToTokens}; +/// # use syn2 as syn; +/// use manyhow::{derive, Emitter, Result}; +/// use proc_macro2::TokenStream; +/// # let item = quote!(); +/// # let output: TokenStream = +/// derive!(item, |item: syn::DeriveInput, +/// dummy: &mut TokenStream, +/// emitter: &mut Emitter| +/// -> Result { +/// // .. +/// # Ok(quote!()) +/// }); +/// ``` +#[macro_export] +macro_rules! derive { + ($item:expr, $impl:expr $(,)?) => { + $crate::__macro_handler! {derive_transparent; item: $item; $impl} + }; +} + +/// Handles function like [`proc_macro`](https://doc.rust-lang.org/reference/procedural-macros.html#function-like-procedural-macros) +/// implementation +/// +/// Use [`function!`] to support [`Parse`] and [`ToTokens`] as well. +/// +/// Takes any `TokenStream` for `input` and returns any +/// `TokenStream`. If `input_as_dummy = true` the item input will be used as +/// default dummy code on error. `body` takes a [`FunctionMacroHandler`] with +/// one `TokenStream` parameter. And an optional [`&mut Emitter`](Emitter) and a +/// `&mut TokenStream` for storing a dummy output. +/// +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// # use quote::{quote, ToTokens}; +/// use manyhow::{function, Emitter, Result}; +/// use proc_macro2::TokenStream; +/// # let input = quote!(); +/// # let output: TokenStream = +/// function( +/// input, +/// false, +/// |input: TokenStream, dummy: &mut TokenStream, emitter: &mut Emitter| -> Result { +/// // .. +/// # Ok(quote!()) +/// }, +/// ); +/// ``` +/// +/// *Note:* When `input_as_dummy = true` the `dummy: &mut TokenStream` will be +/// initialized with `input`. To override assign a new `TokenStream`: +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// use manyhow::{function, Result, SilentError}; +/// use proc_macro2::TokenStream; +/// use quote::{quote, ToTokens}; +/// let input = quote!(some input); +/// let output: TokenStream = function( +/// input, +/// true, +/// |input: TokenStream, +/// dummy: &mut TokenStream| +/// -> Result { +/// assert_tokens!(dummy.to_token_stream(), { +/// some input +/// }); +/// *dummy = quote! { +/// another input +/// }; +/// // .. +/// Err(SilentError) +/// }, +/// ); +/// +/// assert_tokens! {output, {another input}}; +/// ``` +pub fn function< + Input: AnyTokenStream, + Dummy: AnyTokenStream, + Output: MacroOutput, + Return: AnyTokenStream, + Function, +>( + input: impl AnyTokenStream, + input_as_dummy: bool, + body: impl FunctionMacroHandler, +) -> Return { + let mut tokens = if input_as_dummy { + input.clone().into().into() + } else { + Dummy::default() + }; + let mut emitter = Emitter::new(); + let output = body.call(input.into().into(), &mut tokens, &mut emitter); + let mut tokens = tokens.into(); + let mut tokens = match output.convert() { + Ok(tokens) => tokens, + Err(error) => { + error.to_tokens(&mut tokens); + tokens + } + }; + emitter.to_tokens(&mut tokens); + tokens.into() +} + +/// Handles function like [`proc_macro`](https://doc.rust-lang.org/reference/procedural-macros.html#function-like-procedural-macros) +/// implementation +/// +/// Takes any `TokenStream` for `input` and returns any `TokenStream`. If +/// `#[as_dummy]` is specified on input, it will be used as default +/// dummy code on error. `body` takes a [`FunctionMacroHandler`] with one +/// `TokenStream` or type implementing [`Parse`] parameter and returns a +/// `TokenStream` or type implementing [`ToTokens`]. And an optional [`&mut +/// Emitter`](Emitter) and a `&mut TokenStream` for storing a dummy output. +/// +/// ``` +/// # use proc_macro_utils::assert_tokens; +/// # use quote::{quote, ToTokens}; +/// # use syn2 as syn; +/// use manyhow::{function, Emitter, Result}; +/// use proc_macro2::TokenStream; +/// # let input = quote!(); +/// # let output: TokenStream = +/// function!(input, |input: syn::Item, +/// dummy: &mut TokenStream, +/// emitter: &mut Emitter| +/// -> Result { +/// // .. +/// # manyhow::bail!("unimplemented") +/// }); +/// ``` +/// +/// *Note:* When `#[as_dummy]` is specified on the input, the `dummy: &mut +/// TokenStream` will be initialized with `input`. To override assign a new +/// `TokenStream`: +/// +/// ``` +/// use proc_macro_utils::assert_tokens; +/// use manyhow::{function, Result, SilentError}; +/// use proc_macro2::TokenStream; +/// use quote::{quote, ToTokens}; +/// +/// let input = quote!(some input); +/// let output: TokenStream = function!( +/// #[as_dummy] input, +/// |input: TokenStream, dummy: &mut TokenStream| +/// -> Result { +/// assert_tokens!(dummy.to_token_stream(), { +/// some input +/// }); +/// *dummy = quote! { +/// another input +/// }; +/// // .. +/// Err(SilentError) +/// }, +/// ); +/// +/// assert_tokens! {output, {another input}}; +/// ``` +#[macro_export] +macro_rules! function { + (#[as_dummy] $input:expr, $impl:expr $(,)?) => { + $crate::__macro_handler! {function_transparent; input: $input; $impl; dummy: $input} + }; + ($input:expr, $impl:expr $(,)?) => { + $crate::__macro_handler! {function_transparent; input: $input; $impl; dummy} + }; +} + +#[test] +fn function_macro() { + use proc_macro::TokenStream as TokenStream1; + use quote::quote; + // proc_macro2::TokenStream + let output: TokenStream = + function!(quote!(hello), |input: TokenStream| -> TokenStream { input }); + assert_eq!(output.to_string(), "hello"); + // proc_macro::TokenStream do not run :D + if false { + let _: TokenStream1 = function!( + TokenStream1::from(quote!(hello)), + |input: TokenStream1| -> TokenStream1 { input } + ); + } + + #[cfg(feature = "syn2")] + { + use quote::ToTokens; + let output: TokenStream = function!( + #[as_dummy] + quote!(hello;), + |input: syn2::LitInt| -> TokenStream { input.into_token_stream() } + ); + assert_eq!( + output.to_string(), + quote!(hello; ::core::compile_error! { "expected integer literal" }).to_string() + ); + let output: TokenStream = function!(quote!(20), |_input: syn2::LitInt| -> syn2::Ident { + syn2::parse_quote!(hello) + }); + assert_eq!(output.to_string(), "hello"); + } +} + +macro_rules! macro_input { + ($MacroInput:ident; $($input:ident: $Input:ident),+; $a:literal; $name:literal; $token_streams:literal) => { + /// Input of + #[doc = $a] + #[doc = $name] + /// proc-macro + /// + /// Note: for `TokenStream` either [`proc_macro::TokenStream`] or + /// [`proc_macro2::TokenStream`] can be used. + /// + /// Trait is implemented for any [`function`](FnOnce), taking in + #[doc = concat!($token_streams, ".")] + /// Additionally, they can take optionally in any order a [`&mut + /// Emitter`](Emitter) which allows emitting errors without returning early. And + /// a `&mut TokenStream` to return a dummy `TokenStream` on failure. + /// + /// When used with + #[doc = concat!("[`", $name, "()`]")] + /// it must return a type implementing [`MacroOutput`], with + #[doc = concat!("[`", $name, "!`]")] + /// it can accept types implementing [`Parse`] and return types + /// implementing [`ToTokens`](quote::ToTokens). + #[allow(missing_docs)] + pub trait $MacroInput { + $(type $Input;)+ + type Dummy; + type Output; + #[allow(clippy::missing_errors_doc)] + fn call( + self, + $(item: Self::$Input,)+ + dummy: &mut Self::Dummy, + emitter: &mut Emitter, + ) -> Self::Output; + } + + macro_input_impl!([$($Input,)+ Dummy: Clone]; $MacroInput; $($input: $Input),*; &mut Dummy, &mut Emitter; dummy: Dummy dummy; emitter emitter); + macro_input_impl!([$($Input,)+ Dummy: Clone]; $MacroInput; $($input: $Input),*; &mut Dummy; dummy: Dummy dummy; _emitter); + macro_input_impl!([$($Input),+]; $MacroInput; $($input: $Input),*; &mut Emitter; _dummy: TokenStream; emitter emitter); + macro_input_impl!([$($Input),+]; $MacroInput; $($input: $Input),*; ; _dummy: TokenStream; _emitter); + }; +} + +macro_rules! macro_input_impl { + ([$($gen:tt)*]; $MacroInput:ident; $($input:ident: $Input:ident),+; $($Extra:ty),*; $dummy1:ident: $Dummy:ident $($dummy2:ident)?; $emitter1:ident $($emitter2:ident)?) => { + impl<$($gen)*, Output, Function> $MacroInput<($($Input,)+ $($Extra,)* Output)> for Function + where + Function: Fn($($Input,)+ $($Extra,)*) -> Output, + { + type Dummy = $Dummy; + $(type $Input = $Input;)* + type Output = Output; + + fn call( + self, + $($input: Self::$Input,)* + $dummy1: &mut Self::Dummy, + $emitter1: &mut Emitter, + ) -> Self::Output { + self($($input,)+ $($dummy2,)? $($emitter2,)?) + } + } + + } +} + +macro_input!(FunctionMacroHandler; input: Input; "a"; "function"; "one `TokenStream`"); +macro_input!(DeriveMacroHandler; item: Item; "a"; "derive"; "one `TokenStream`"); +macro_input!(AttributeMacroHandler; input: Input, item: Item; "an"; "attribute"; "two `TokenStream`s"); + +#[rustfmt::skip] +#[allow(clippy::doc_markdown)] +/// Output of a macro handler. +/// +/// Enables support for returning any [`TokenStream`](AnyTokenStream) or +/// [Result]<[TokenStream](AnyTokenStream), [impl ToTokensError](ToTokensError)> +/// from a proc-macro implementation. +pub trait MacroOutput { + /// Handles conversion into a [Result]<[TokenStream](AnyTokenStream), [Error]>. + #[allow(clippy::missing_errors_doc)] + fn convert(self) -> Result; +} + +impl MacroOutput for T { + fn convert(self) -> Result { + Ok(self.into()) + } +} + +impl MacroOutput for Result { + fn convert(self) -> Result { + self.map_err(Error::from).and_then(MacroOutput::convert) + } +} diff --git a/third-party/manyhow/src/macro_rules.rs b/third-party/manyhow/src/macro_rules.rs new file mode 100644 index 0000000..e29d222 --- /dev/null +++ b/third-party/manyhow/src/macro_rules.rs @@ -0,0 +1,323 @@ +#[cfg(doc)] +use proc_macro2::Span; +#[cfg(doc)] +use quote::ToTokens; + +#[cfg(doc)] +use crate::{Emitter, Error, ErrorMessage, SpanRanged}; + +#[doc(hidden)] +#[macro_export] +macro_rules! __error_message_internal { + ((cs($($fmt:tt)*)$(.$fn:ident($($fmt_fn:tt)*))*), (), ()) => { + $crate::ErrorMessage::call_site($($fmt)*) + $(.attachment(::core::stringify!($fn), $($fmt_fn)*))* + }; + ((new($span:expr)($($fmt:tt)*)$(.$fn:ident($($fmt_fn:tt)*))*), (), ()) => { + $crate::ErrorMessage::new( + $crate::span_range!($span), + $($fmt)* + ) + $(.attachment(::core::stringify!($fn), $($fmt_fn)*))* + }; + // ident = expr + ($head:tt, ($($fmt:tt)*), (, $ident:ident = $expr:expr, $($tail:tt)*)) => { + $crate::__error_message_internal!($head, ($($fmt)*, $ident = $expr), (, $($tail)*)) + }; + ($head:tt, ($($fmt:tt)*), (, $ident:ident = $expr:expr; $($tail:tt)*)) => { + $crate::__error_message_internal!($head, ($($fmt)*, $ident = $expr), (; $($tail)*)) + }; + ($head:tt, ($($fmt:tt)*), (, $ident:ident = $expr:expr)) => { + $crate::__error_message_internal!($head, ($($fmt)*, $ident = $expr), ()) + }; + // expr, + ($head:tt, ($($fmt:tt)*), (, $expr:expr, $($tail:tt)*)) => { + $crate::__error_message_internal!($head, ($($fmt)*, $expr), (, $($tail)*)) + }; + ($head:tt, ($($fmt:tt)*), (, $expr:expr; $($tail:tt)*)) => { + $crate::__error_message_internal!($head, ($($fmt)*, $expr), (; $($tail)*)) + }; + ($head:tt, ($($fmt:tt)*), (, $expr:expr)) => { + $crate::__error_message_internal!($head, ($($fmt)*, $expr), ()) + }; + // ; ident = "format", arguments + (($($head:tt)*), $fmt:tt, ($(,)?$(;)?)) => { + $crate::__error_message_internal!(($($head)*(::core::format_args!$fmt)), (), ()) + }; + (($($head:tt)*), $fmt:tt, ($(,)?; $attachment:ident = $fmt_str:literal $($tail:tt)*)) => { + $crate::__error_message_internal!(($($head)*(::core::format_args!$fmt).$attachment), ($fmt_str), ($($tail)*)) + }; +} + +/// Creates an [`ErrorMessage`], comparable to the [`anyhow!`](https://docs.rs/anyhow/latest/anyhow/macro.anyhow.html) macro +/// +/// If the first argument is not a literal it is taken as the span of the error. +/// The span expression can **either** implement [`SpanRanged`] or implement +/// [`ToTokens`]. Otherwise, [`Span::call_site`] is used. +/// +/// ``` +/// # use proc_macro2::Span; +/// # use quote::quote; +/// # use manyhow::error_message; +/// assert_eq!( +/// error_message!("format {} string{named}", "<3", named = "!").to_string(), +/// "format <3 string!" +/// ); +/// // Span can either be `proc_macro::Span` or `proc_macro2::Span` +/// assert_eq!( +/// error_message!(Span::call_site(), "spanned error").to_string(), +/// "spanned error" +/// ); +/// # if false { +/// // Or any expression implementing `quote::ToTokens` +/// assert_eq!( +/// error_message!(quote!(some tokens), "spanned error").to_string(), +/// "spanned error" +/// ); +/// # } +/// ``` +/// +/// On top of the standard [`format_args!`] parameters additional attachments +/// can be specified delimited with `;`. +/// +/// ``` +/// # use proc_macro2::Span; +/// # use quote::quote; +/// # use manyhow::error_message; +/// assert_eq!( +/// error_message!( +/// "format {} string{named}", "<3", named = "!"; +/// error = "some additional error"; +/// info = "some info as well"; +/// custom_attachment = "amazing" +/// ).to_string(), +/// "format <3 string! +/// +/// = error: some additional error +/// = info: some info as well +/// = custom_attachment: amazing +/// " +/// ); +/// ``` +#[macro_export] +macro_rules! error_message { + ($fmt:literal $($tt:tt)*) => { + $crate::__error_message_internal!((cs), ($fmt), ($($tt)*)) + }; + ($span:expr, $fmt:literal $($tt:tt)*) => { + $crate::__error_message_internal!((new($span)), ($fmt), ($($tt)*)) + }; +} + +/// Exit by returning error, matching [`anyhow::bail!`](https://docs.rs/anyhow/latest/anyhow/macro.bail.html). +/// +/// The syntax is identical to [`error_message!`], the only difference is, that +/// a single expression with an error is supported as well. +/// ```should_panic +/// # use manyhow::bail; +/// # use proc_macro2::Span; +/// # use syn2 as syn; +/// bail!("an error message"; error = "with attachments"); +/// let span = Span::call_site(); +/// bail!(span, "error message"); +/// let error = syn::Error::new(Span::call_site(), "an error"); +/// bail!(error); +/// # Ok::<_, manyhow::Error>(()) +/// ``` +#[macro_export] +macro_rules! bail { + ($msg:literal) => { + return ::core::result::Result::Err($crate::error_message!($msg).into()) + }; + ($error:expr) => { + return ::core::result::Result::Err($error.into()) + }; + ($($tt:tt)*) => { + return ::core::result::Result::Err($crate::error_message!($($tt)*).into()) + }; +} + +/// Return early with an error, if a condition is not satisfied, matching +/// [`anyhow::ensure!`](https://docs.rs/anyhow/latest/anyhow/macro.ensure.html). +/// +/// The syntax is identical to [`bail!`], with an additional leading condition. +/// +/// Additional to a boolean expression, the expression can also be a `let ... = +/// ...` pattern matching, and will expand to `let ... else`. +/// ``` +/// # use manyhow::ensure; +/// ensure!(true, "an error message"; help = "with attachments"); +/// +/// ensure!(let Some(a) = Some(1), "error"); +/// assert_eq!(a, 1); +/// +/// # Ok::<_, manyhow::Error>(()) +/// ``` +/// ```should_panic +/// # use manyhow::ensure; +/// # use proc_macro2::Span; +/// # use syn2 as syn; +/// let span = Span::call_site(); +/// ensure!(false, span, "error message"); +/// let error = syn::Error::new(Span::call_site(), "an error"); +/// ensure!(false, error); +/// # Ok::<_, manyhow::Error>(()) +/// ``` +#[macro_export] +macro_rules! ensure { + ($cond:expr, $($bail_args:tt)*) => { + if !$cond { + $crate::bail!($($bail_args)*); + } + }; + (let $pat:pat = $expr:expr, $($bail_args:tt)*) => { + let $pat = $expr else { + $crate::bail!($($bail_args)*); + }; + }; +} + +/// Push an error to an emitter. +/// +/// The syntax is identical to [`error_message!`] and [`bail!`], but the first +/// argument is the [`Emitter`]. +/// ``` +/// # use manyhow::{emit, Emitter}; +/// # use proc_macro2::Span; +/// # use syn2 as syn; +/// let mut emitter = Emitter::new(); +/// emit!(emitter, "an error message"); +/// emit!(emitter, "an error message"; error = "with attachments"); +/// let span = Span::call_site(); +/// emit!(emitter, span, "error message"); +/// let error = syn::Error::new(Span::call_site(), "an error"); +/// emit!(emitter, error); +/// ``` +/// +/// It can also be used with [`Error`]. +/// ``` +/// # use manyhow::{emit, error_message, Error}; +/// # use proc_macro2::Span; +/// # use syn2 as syn; +/// let mut error: Error = error_message!("initial error").into(); +/// emit!(error, "an error message"); +/// ``` +/// +/// Or any collection implementing [`Extend`]. +/// ``` +/// # use manyhow::emit; +/// # use proc_macro2::Span; +/// # use syn2 as syn; +/// let mut errors = Vec::new(); +/// emit!(errors, "an error message"); +/// ``` +#[macro_export] +macro_rules! emit { + ($emitter:expr, $msg:literal) => { + $emitter.extend(::core::iter::once::<$crate::ErrorMessage>($crate::error_message!($msg))); + }; + ($emitter:expr, $error:expr) => { + $emitter.extend(::core::iter::once($error)); + }; + ($emitter:expr, $($tt:tt)*) => { + $emitter.extend(::core::iter::once::<$crate::ErrorMessage>($crate::error_message!($($tt)*).into())); + }; +} + +#[cfg(test)] +mod test { + use proc_macro::Span; + use quote::quote; + + use crate::{Emitter, ErrorMessage}; + + macro_rules! returned { + ($ty:ty, $expr:expr) => { + #[allow(unreachable_code)] + (|| -> $ty { + $expr; + unreachable!(); + })() + }; + } + #[test] + fn bail() { + assert_eq!( + returned!(Result<(), ErrorMessage>, bail!("format")) + .unwrap_err() + .to_string(), + "format" + ); + assert_eq!( + returned!(Result<(), ErrorMessage>, bail!("format {}", 1)) + .unwrap_err() + .to_string(), + "format 1" + ); + let b = "ho"; + assert_eq!( + returned!(Result<(), ErrorMessage>, bail!("format {} {a} {} {b}", 1, 2, a = 4)) + .unwrap_err() + .to_string(), + "format 1 4 2 ho" + ); + } + + #[test] + fn error_message() { + assert_eq!(error_message!("test").to_string(), "test"); + assert_eq!(error_message!("test";).to_string(), "test"); + assert_eq!( + error_message!( + "test"; + error = "hello {} {a}", 1 + 4, a = "" + ) + .to_string(), + "test\n\n = error: hello 5 \n" + ); + assert_eq!( + error_message!( + "test"; + error = "hello {} {a}", 1 + 4, a = ""; + ) + .to_string(), + "test\n\n = error: hello 5 \n" + ); + assert_eq!( + error_message!( + "test"; + error = "hello {} {a}", 1 + 4, a = "",; + hint = "a hint" + ) + .to_string(), + "test\n\n = error: hello 5 \n = hint: a hint\n" + ); + } + + #[test] + fn emit() { + let mut emitter = Emitter::new(); + emit!(emitter, "an error message"; error = "with attachments"); + let span = proc_macro2::Span::call_site(); + emit!(emitter, span, "error message"); + #[cfg(feature = "syn2")] + { + let error = syn2::Error::new(proc_macro2::Span::call_site(), "an error"); + emit!(emitter, error); + } + } + + // Only tests that it compiles + fn _error_message_spanned() { + let span = Span::call_site(); + _ = error_message!(span, "test"); + + let span = proc_macro::Span::call_site(); + _ = error_message!(span, "test"); + + let tokens = quote!(test); + _ = error_message!(tokens, "an error message",); + _ = error_message!(tokens, "an error message",;); + _ = error_message!(tokens, "an error message",; warning="and a warning";); + } +} diff --git a/third-party/manyhow/src/parse_to_tokens.rs b/third-party/manyhow/src/parse_to_tokens.rs new file mode 100644 index 0000000..8c665b3 --- /dev/null +++ b/third-party/manyhow/src/parse_to_tokens.rs @@ -0,0 +1,172 @@ +#![allow(missing_docs, clippy::pedantic)] +use std::convert::Infallible; +use std::marker::PhantomData; + +use proc_macro2::TokenStream; + +use crate::{ + AnyTokenStream, AttributeMacroHandler, DeriveMacroHandler, Emitter, FunctionMacroHandler, + ToTokensError, +}; +pub trait ManyhowParse { + fn manyhow_parse(&self, input: impl AnyTokenStream, attr: bool) -> Result; +} +pub trait ManyhowToTokens { + fn manyhow_to_tokens(&self, input: T, tokens: &mut TokenStream); +} +pub trait ManyhowTry { + type Ok; + type Err; + fn manyhow_try(&self, value: T) -> Result; +} + +pub struct WhatType(PhantomData); + +impl WhatType { + /// Always panics + pub fn identify(&self) -> Result { + unimplemented!("DON'T YOU DARE CALL ME") + } + + pub fn from(_ty: &T) -> Self { + Self(PhantomData) + } + + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self(PhantomData) + } +} + +impl Clone for WhatType { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for WhatType {} + +impl + From> ManyhowParse for WhatType { + fn manyhow_parse(&self, input: impl AnyTokenStream, _attr: bool) -> Result { + Ok(input.into().into()) + } +} + +impl ManyhowToTokens for WhatType { + fn manyhow_to_tokens(&self, input: TokenStream, tokens: &mut TokenStream) { + tokens.extend(input); + } +} + +impl ManyhowToTokens for WhatType { + fn manyhow_to_tokens(&self, input: proc_macro::TokenStream, tokens: &mut TokenStream) { + tokens.extend(TokenStream::from(input)); + } +} + +impl ManyhowToTokens for WhatType { + fn manyhow_to_tokens(&self, input: E, tokens: &mut TokenStream) { + input.to_tokens(tokens); + } +} + +impl ManyhowTry> for WhatType> { + type Err = E; + type Ok = T; + + fn manyhow_try(&self, value: Result) -> Result { + value + } +} + +impl ManyhowTry for &WhatType { + type Err = Infallible; + type Ok = T; + + fn manyhow_try(&self, value: T) -> Result { + Ok(value) + } +} + +#[cfg(feature = "syn2")] +impl ManyhowParse for &WhatType { + fn manyhow_parse(&self, input: impl AnyTokenStream, attr: bool) -> Result { + let input = input.into(); + let empty = input.is_empty(); + syn2::parse2(input).map_err(|e| { + let mut e = e.into_compile_error(); + if attr && empty { + error_message!("while parsing attribute argument (`#[... (...)]`)") + .to_tokens(&mut e) + } + e + }) + } +} +#[cfg(feature = "syn2")] +impl ManyhowToTokens for &WhatType { + fn manyhow_to_tokens(&self, input: T, tokens: &mut TokenStream) { + input.to_tokens(tokens); + } +} + +#[cfg(feature = "syn2")] +#[test] +#[allow(unused)] +fn test_inference() { + use syn2::parse::Parse; + + if false { + let wt = &WhatType::new(); + let ts: proc_macro::TokenStream = wt.manyhow_parse(quote::quote!(test), false).unwrap(); + let wt = &WhatType::new(); + if false { + let wt: Result = wt.identify(); + } + let ts: syn2::Ident = wt.manyhow_parse(quote::quote!(test), false).unwrap(); + + struct Parsable; + impl Parse for Parsable { + fn parse(input: syn2::parse::ParseStream) -> syn2::Result { + todo!() + } + } + let wt = &WhatType::new(); + let _: Result = wt.identify(); + let ts = wt.manyhow_parse(quote::quote!(test), false).unwrap(); + } +} + +macro_rules! transparent_handlers { + ($name:ident; $MacroInput:ident; $($input:ident: $Input:ident $($context:expr)?),*; $($dummy:ident)?) => { + /// Internal implementation for macro. + pub fn $name<$($Input,)* Dummy: AnyTokenStream, Output, Function,>( + $($input: Result<$Input, TokenStream>,)* + $($dummy: Option,)? + body: impl $MacroInput, + ) -> Result<(Output, TokenStream, TokenStream), TokenStream> { + // use $crate::ToTokensError as _; + #[allow(unused)] + let mut dummy = TokenStream::new(); + $(let mut dummy = $dummy.unwrap_or_default().into();)? + $(let $input = match $input { + Ok($input) => $input, + Err(tokens) => { + dummy.extend(tokens); + $($crate::error_message!($context).to_tokens(&mut dummy);)? + return Err(dummy); + } + };)* + let mut dummy = dummy.into(); + let mut emitter = Emitter::new(); + let output = body.call($($input,)+ &mut dummy, &mut emitter); + let mut tokens = TokenStream::new(); + emitter.to_tokens(&mut tokens); + Ok((output, tokens, dummy.into())) + } + }; +} + +transparent_handlers! { function_transparent; FunctionMacroHandler; input: Input; dummy } +transparent_handlers! { derive_transparent; DeriveMacroHandler; item: Item;} +transparent_handlers! { attribute_transparent; AttributeMacroHandler; input: Input, item: Item; dummy } diff --git a/third-party/manyhow/src/span_ranged.rs b/third-party/manyhow/src/span_ranged.rs new file mode 100644 index 0000000..221b883 --- /dev/null +++ b/third-party/manyhow/src/span_ranged.rs @@ -0,0 +1,208 @@ +use std::ops::Range; + +use proc_macro2::Span; +use quote::ToTokens; + +#[cfg(doc)] +use crate::ErrorMessage; + +/// Get a [`Range`](std::ops::Range)[``](proc_macro2::Span) from a +/// type that **either** implements [`SpanRanged`] or [`ToTokens`] (**NOT** +/// both). +#[macro_export] +macro_rules! span_range { + ($span:expr) => {{ + // Warning is triggered if span is incorrect type + #[allow(unused_imports)] + use $crate::__private::*; + ($span).FIRST_ARG_MUST_IMPLEMENT_SpanRanged_OR_ToTokens() + }}; +} + +/// Returns the [`Range`](Range)[``](Span) from the start to the end of +/// multi-token structures. +/// +/// `start` and `end` can be the same when called on single Tokens or [`Span`]. +/// +/// Due to compiler limitations, it is currently not possible to implement +/// `SpanRanged for T: ToTokens`, therefor there is +/// [`to_tokens_span_range()`]. +/// +/// For types that **either** implement [`SpanRanged`] or [`ToTokens`] (but +/// **NOT** both) the [`span_range!`] macro can be used as well. +/// +/// # Motivation +/// This is superior to a normal [`Span`] (at least until [`Span::join`] works +/// on stable), because it leads to better error messages: +/// +/// Given the following expression +/// ``` +/// let a = |something: usize| something; +/// ``` +/// +/// [`ErrorMessage::new(first_pipe_span, "error message")`](ErrorMessage::new) +/// would result in something like +/// +/// ```text +/// error: error message +/// +/// let a = |something: usize| something; +/// ^ +/// ``` +/// +/// While [`ErrorMessage::new(first_pipe_span..something_span, +/// "error message")`](ErrorMessage::new) would improve the error message to: +/// ```text +/// error: error message +/// +/// let a = |something: usize| something; +/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/// ``` +pub trait SpanRanged { + /// Returns the [`Range`](Range)[``](Span) fully encompasing `self` + fn span_range(&self) -> Range; + + /// Returns [`Self::span_range`] as a single span if possible, currently + /// only possible on nightly. [more](proc_macro2::Span::join) + fn span_joined(&self) -> Option { + let range = self.span_range(); + range.start.join(range.end) + } + + #[doc(hidden)] + #[deprecated] + fn joined(&self) -> Option { + self.span_joined() + } +} + +impl SpanRanged for &T { + fn span_range(&self) -> Range { + (*self).span_range() + } +} + +impl SpanRanged for Option { + fn span_range(&self) -> Range { + self.as_ref() + .map_or_else(|| Span::call_site().span_range(), SpanRanged::span_range) + } +} + +impl SpanRanged for (A, B) { + fn span_range(&self) -> Range { + (self.0.span_range().start)..(self.1.span_range().end) + } +} + +impl SpanRanged for Span { + fn span_range(&self) -> Range { + *self..*self + } +} +impl SpanRanged for proc_macro::Span { + fn span_range(&self) -> Range { + (*self).into()..(*self).into() + } +} + +impl SpanRanged for Range { + fn span_range(&self) -> Range { + self.start.span_range().start..self.end.span_range().end + } +} + +impl SpanRanged for proc_macro::TokenStream { + fn span_range(&self) -> Range { + let mut this = self.clone().into_iter(); + let first = this + .next() + .as_ref() + .map_or_else(proc_macro::Span::call_site, proc_macro::TokenTree::span); + + let last = this + .last() + .as_ref() + .map_or(first, proc_macro::TokenTree::span); + first.into()..last.into() + } +} + +/// Implementation of [`SpanRanged`](SpanRanged)` for T: `[`ToTokens`] +/// +/// This is necessary to put in a standalone function due to compiler +/// limitations. +pub fn to_tokens_span_range(tokens: impl ToTokens) -> Range { + proc_macro::TokenStream::from(tokens.to_token_stream()).span_range() +} + +#[doc(hidden)] +pub trait SpanRangedToSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_IMPLEMENT_SpanRanged_OR_ToTokens(&self) -> Range; +} +impl SpanRangedToSpanRange for T { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_IMPLEMENT_SpanRanged_OR_ToTokens(&self) -> Range { + self.span_range() + } +} + +#[doc(hidden)] +pub trait ToTokensToSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_IMPLEMENT_SpanRanged_OR_ToTokens(&self) -> Range; +} +impl ToTokensToSpanRange for T { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_IMPLEMENT_SpanRanged_OR_ToTokens(&self) -> Range { + let mut this = self.to_token_stream().into_iter(); + let first = this + .next() + .as_ref() + .map_or_else(proc_macro2::Span::call_site, proc_macro2::TokenTree::span); + + let last = this + .last() + .as_ref() + .map_or(first, proc_macro2::TokenTree::span); + first..last + } +} + +#[doc(hidden)] +pub trait ToTokensTupleToSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_IMPLEMENT_SpanRanged_OR_ToTokens(&self) -> Range; +} +impl ToTokensTupleToSpanRange for (A, B) { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_IMPLEMENT_SpanRanged_OR_ToTokens(&self) -> Range { + let first = self + .0 + .to_token_stream() + .into_iter() + .next() + .as_ref() + .map_or_else(proc_macro2::Span::call_site, proc_macro2::TokenTree::span); + + let last = self + .1 + .to_token_stream() + .into_iter() + .last() + .as_ref() + .map_or(first, proc_macro2::TokenTree::span); + + first..last + } +} + +#[cfg(test)] +mod test { + #[test] + fn test() { + span_range!(1); + span_range!((1, 2)); + } +} diff --git a/third-party/proc-macro-error2/.gitignore b/third-party/proc-macro-error2/.gitignore new file mode 100644 index 0000000..5e81b66 --- /dev/null +++ b/third-party/proc-macro-error2/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +.fuse_hidden* diff --git a/third-party/proc-macro-error2/CHANGELOG.md b/third-party/proc-macro-error2/CHANGELOG.md new file mode 100644 index 0000000..4f22c85 --- /dev/null +++ b/third-party/proc-macro-error2/CHANGELOG.md @@ -0,0 +1,180 @@ +# v2.0.1 (2024-09-06) + +* Fixed a span location issue due to mistake in refactoring (#2) + +# v2.0.0 (2024-09-05) + +No changes, simply releasing pre-release as full release. + +# v2.0.0-pre.1 (2024-09-01) + +* __Crate has been renamed to `proc-macro-error2`, due to the old maintainer's inactivity.__ + +* `syn` has been upgraded to `2` +* MSRV has been bumped to `1.61` +* Warnings have been fixed, including `clippy::pedantic` lints +* CI has been converted to GitHub actions, and testing infrastructure significantly simplified. +* Automatic nightly detection has been removed, use the `nightly` feature for improved diagnostics at the cost of stablity. + +# v1.0.4 (2020-7-31) + +* `SpanRange` facility is now public. +* Docs have been improved. +* Introduced the `syn-error` feature so you can opt-out from the `syn` dependency. + +# v1.0.3 (2020-6-26) + +* Corrected a few typos. +* Fixed the `emit_call_site_warning` macro. + +# v1.0.2 (2020-4-9) + +* An obsolete note was removed from documentation. + +# v1.0.1 (2020-4-9) + +* `proc-macro-hack` is now well tested and supported. Not sure about `proc-macro-nested`, + please fill a request if you need it. +* Fixed `emit_call_site_error`. +* Documentation improvements. + +# v1.0.0 (2020-3-25) + +I believe the API can be considered stable because it's been a few months without +breaking changes, and I also don't think this crate will receive much further evolution. +It's perfect, admit it. + +Hence, meet the new, stable release! + +### Improvements + +* Supported nested `#[proc_macro_error]` attributes. Well, you aren't supposed to do that, + but I caught myself doing it by accident on one occasion and the behavior was... surprising. + Better to handle this smooth. + +# v0.4.12 (2020-3-23) + +* Error message on macros' misuse is now a bit more understandable. + +# v0.4.11 (2020-3-02) + +* `build.rs` no longer fails when `rustc` date could not be determined, + (thanks to [`Fabian Möller`](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/8) + for noticing and to [`Igor Gnatenko`](https://gitlab.com/CreepySkeleton/proc-macro-error/-/merge_requests/25) + for fixing). + +# v0.4.10 (2020-2-29) + +* `proc-macro-error` doesn't depend on syn\[full\] anymore, the compilation + is \~30secs faster. + +# v0.4.9 (2020-2-13) + +* New function: `append_dummy`. + +# v0.4.8 (2020-2-01) + +* Support for children messages + +# v0.4.7 (2020-1-31) + +* Now any type that implements `quote::ToTokens` can be used instead of spans. + This allows for high quality error messages. + +# v0.4.6 (2020-1-31) + +* `From` implementation doesn't lose span info anymore, see + [#6](https://gitlab.com/CreepySkeleton/proc-macro-error/issues/6). + +# v0.4.5 (2020-1-20) +Just a small intermediate release. + +* Fix some bugs. +* Populate license files into subfolders. + +# v0.4.4 (2019-11-13) +* Fix `abort_if_dirty` + warnings bug +* Allow trailing commas in macros + +# v0.4.2 (2019-11-7) +* FINALLY fixed `__pme__suggestions not found` bug + +# v0.4.1 (2019-11-7) YANKED +* Fixed `__pme__suggestions not found` bug +* Documentation improvements, links checked + +# v0.4.0 (2019-11-6) YANKED + +## New features +* "help" messages that can have their own span on nightly, they + inherit parent span on stable. + ```rust + let cond_help = if condition { Some("some help message") else { None } }; + abort!( + span, // parent span + "something's wrong, {} wrongs in total", 10; // main message + help = "here's a help for you, {}", "take it"; // unconditional help message + help =? cond_help; // conditional help message, must be Option + note = note_span => "don't forget the note, {}", "would you?" // notes can have their own span but it's effective only on nightly + ) + ``` +* Warnings via `emit_warning` and `emit_warning_call_site`. Nightly only, they're ignored on stable. +* Now `proc-macro-error` delegates to `proc_macro::Diagnostic` on nightly. + +## Breaking changes +* `MacroError` is now replaced by `Diagnostic`. Its API resembles `proc_macro::Diagnostic`. +* `Diagnostic` does not implement `From<&str/String>` so `Result::abort_or_exit()` + won't work anymore (nobody used it anyway). +* `macro_error!` macro is replaced with `diagnostic!`. + +## Improvements +* Now `proc-macro-error` renders notes exactly just like rustc does. +* We don't parse a body of a function annotated with `#[proc_macro_error]` anymore, + only looking at the signature. This should somewhat decrease expansion time for large functions. + +# v0.3.3 (2019-10-16) +* Now you can use any word instead of "help", undocumented. + +# v0.3.2 (2019-10-16) +* Introduced support for "help" messages, undocumented. + +# v0.3.0 (2019-10-8) + +## The crate has been completely rewritten from scratch! + +## Changes (most are breaking): +* Renamed macros: + * `span_error` => `abort` + * `call_site_error` => `abort_call_site` +* `filter_macro_errors` was replaced by `#[proc_macro_error]` attribute. +* `set_dummy` now takes `TokenStream` instead of `Option` +* Support for multiple errors via `emit_error` and `emit_call_site_error` +* New `macro_error` macro for building errors in format=like style. +* `MacroError` API had been reconsidered. It also now implements `quote::ToTokens`. + +# v0.2.6 (2019-09-02) +* Introduce support for dummy implementations via `dummy::set_dummy` +* `multi::*` is now deprecated, will be completely rewritten in v0.3 + +# v0.2.0 (2019-08-15) + +## Breaking changes +* `trigger_error` replaced with `MacroError::trigger` and `filter_macro_error_panics` + is hidden from docs. + This is not quite a breaking change since users weren't supposed to use these functions directly anyway. +* All dependencies are updated to `v1.*`. + +## New features +* Ability to stack multiple errors via `multi::MultiMacroErrors` and emit them at once. + +## Improvements +* Now `MacroError` implements `std::fmt::Display` instead of `std::string::ToString`. +* `MacroError::span` inherent method. +* `From for proc_macro/proc_macro2::TokenStream` implementations. +* `AsRef/AsMut for MacroError` implementations. + +# v0.1.x (2019-07-XX) + +## New features +* An easy way to report errors inside within a proc-macro via `span_error`, + `call_site_error` and `filter_macro_errors`. diff --git a/third-party/proc-macro-error2/Cargo.toml b/third-party/proc-macro-error2/Cargo.toml new file mode 100644 index 0000000..f623e6c --- /dev/null +++ b/third-party/proc-macro-error2/Cargo.toml @@ -0,0 +1,91 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.61" +name = "proc-macro-error2" +version = "2.0.1" +authors = [ + "CreepySkeleton ", + "GnomedDev ", +] +build = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Almost drop-in replacement to panics in proc-macros" +readme = "README.md" +keywords = [ + "proc-macro", + "error", + "errors", +] +categories = ["development-tools::procedural-macro-helpers"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/GnomedDev/proc-macro-error-2" + +[lib] +name = "proc_macro_error2" +path = "src/lib.rs" + +[[test]] +name = "macro-errors" +path = "tests/macro-errors.rs" + +[[test]] +name = "ok" +path = "tests/ok.rs" + +[[test]] +name = "runtime-errors" +path = "tests/runtime-errors.rs" + +[dependencies.proc-macro-error-attr2] +version = "=2.0.0" + +[dependencies.proc-macro2] +version = "1" + +[dependencies.quote] +version = "1" + +[dependencies.syn] +version = "2" +optional = true +default-features = false + +[dev-dependencies.syn] +version = "2" +features = ["full"] + +[dev-dependencies.trybuild] +version = "1.0.99" +features = ["diff"] + +[features] +default = ["syn-error"] +nightly = [] +syn-error = ["dep:syn"] + +[lints.clippy.module_name_repetitions] +level = "allow" +priority = 0 + +[lints.clippy.pedantic] +level = "warn" +priority = -1 + +[lints.rust.unexpected_cfgs] +level = "warn" +priority = 0 +check-cfg = ["cfg(run_ui_tests)"] diff --git a/third-party/proc-macro-error2/Cargo.toml.orig b/third-party/proc-macro-error2/Cargo.toml.orig new file mode 100644 index 0000000..fcc6161 --- /dev/null +++ b/third-party/proc-macro-error2/Cargo.toml.orig @@ -0,0 +1,41 @@ +[package] +name = "proc-macro-error2" +authors = [ + "CreepySkeleton ", + "GnomedDev ", +] +version = "2.0.1" +description = "Almost drop-in replacement to panics in proc-macros" +repository = "https://github.com/GnomedDev/proc-macro-error-2" +rust-version = "1.61" +keywords = ["proc-macro", "error", "errors"] +categories = ["development-tools::procedural-macro-helpers"] +license = "MIT OR Apache-2.0" +edition = "2021" + +[dependencies] +quote = "1" +proc-macro2 = "1" +proc-macro-error-attr2 = { path = "./proc-macro-error-attr", version = "=2.0.0" } + +[dependencies.syn] +version = "2" +optional = true +default-features = false + +[dev-dependencies] +test-crate = { path = "./test-crate" } +syn = { version = "2", features = ["full"] } +trybuild = { version = "1.0.99", features = ["diff"] } + +[features] +default = ["syn-error"] +syn-error = ["dep:syn"] +nightly = [] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(run_ui_tests)'] } + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = { level = "allow" } diff --git a/third-party/proc-macro-error2/LICENSE-APACHE b/third-party/proc-macro-error2/LICENSE-APACHE new file mode 100644 index 0000000..658240a --- /dev/null +++ b/third-party/proc-macro-error2/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2019-2020 CreepySkeleton + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/third-party/proc-macro-error2/LICENSE-MIT b/third-party/proc-macro-error2/LICENSE-MIT new file mode 100644 index 0000000..fc73e59 --- /dev/null +++ b/third-party/proc-macro-error2/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 CreepySkeleton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third-party/proc-macro-error2/README.md b/third-party/proc-macro-error2/README.md new file mode 100644 index 0000000..0910eac --- /dev/null +++ b/third-party/proc-macro-error2/README.md @@ -0,0 +1,250 @@ +# Makes error reporting in procedural macros nice and easy + +[![docs.rs](https://docs.rs/proc-macro-error2/badge.svg)](https://docs.rs/proc-macro-error2) +[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) + +This crate aims to make error reporting in proc-macros simple and easy to use. +Migrate from `panic!`-based errors for as little effort as possible! + +Also, you can explicitly [append a dummy token stream][crate::dummy] to your errors. + +To achieve this, this crate serves as a tiny shim around `proc_macro::Diagnostic` and +`compile_error!`. It detects the most preferable way to emit errors based on compiler's version. +When the underlying diagnostic type is finally stabilized, this crate will be simply +delegating to it, requiring no changes in your code! + +So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality +available on stable ahead of time and your error-reporting code future-proof. + +```toml +[dependencies] +proc-macro-error2 = "2.0" +``` + +*Supports rustc 1.61 and up* + +[Documentation and guide][guide] + +## Quick example + +Code: + +```rust +#[proc_macro] +#[proc_macro_error] +pub fn make_fn(input: TokenStream) -> TokenStream { + let mut input = TokenStream2::from(input).into_iter(); + let name = input.next().unwrap(); + if let Some(second) = input.next() { + abort! { second, + "I don't like this part!"; + note = "I see what you did there..."; + help = "I need only one part, you know?"; + } + } + + quote!( fn #name() {} ).into() +} +``` + +This is how the error is rendered in a terminal: + +

+ +

+ +And this is what your users will see in their IDE: + +

+ +

+ +## Examples + +### Panic-like usage + +```rust +use proc_macro_error2::{ + proc_macro_error, + abort, + abort_call_site, + ResultExt, + OptionExt, +}; +use proc_macro::TokenStream; +use syn::{DeriveInput, parse_macro_input}; +use quote::quote; + +// This is your main entry point +#[proc_macro] +// This attribute *MUST* be placed on top of the #[proc_macro] function +#[proc_macro_error] +pub fn make_answer(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + if let Err(err) = some_logic(&input) { + // we've got a span to blame, let's use it + // This immediately aborts the proc-macro and shows the error + // + // You can use `proc_macro::Span`, `proc_macro2::Span`, and + // anything that implements `quote::ToTokens` (almost every type from + // `syn` and `proc_macro2`) + abort!(err, "You made an error, go fix it: {}", err.msg); + } + + // `Result` has some handy shortcuts if your error type implements + // `Into`. `Option` has one unconditionally. + more_logic(&input).expect_or_abort("What a careless user, behave!"); + + if !more_logic_for_logic_god(&input) { + // We don't have an exact location this time, + // so just highlight the proc-macro invocation itself + abort_call_site!( + "Bad, bad user! Now go stand in the corner and think about what you did!"); + } + + // Now all the processing is done, return `proc_macro::TokenStream` + quote!(/* stuff */).into() +} +``` + +### `proc_macro::Diagnostic`-like usage + +```rust +use proc_macro_error2::*; +use proc_macro::TokenStream; +use syn::{spanned::Spanned, DeriveInput, ItemStruct, Fields, Attribute , parse_macro_input}; +use quote::quote; + +fn process_attrs(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter_map(|attr| match process_attr(attr) { + Ok(res) => Some(res), + Err(msg) => { + emit_error!(attr, "Invalid attribute: {}", msg); + None + } + }) + .collect() +} + +fn process_fields(_attrs: &Fields) -> Vec { + // processing fields in pretty much the same way as attributes + unimplemented!() +} + +#[proc_macro] +#[proc_macro_error] +pub fn make_answer(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + let attrs = process_attrs(&input.attrs); + + // abort right now if some errors were encountered + // at the attributes processing stage + abort_if_dirty(); + + let fields = process_fields(&input.fields); + + // no need to think about emitted errors + // #[proc_macro_error] will handle them for you + // + // just return a TokenStream as you normally would + quote!(/* stuff */).into() +} +``` + +## Real world examples + +* [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) + (abort-like usage) +* [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) + +## Limitations + +- Warnings are emitted only on nightly, they are ignored on stable. +- "help" suggestions can't have their own span info on stable, + (essentially inheriting the parent span). +- If your macro happens to trigger a panic, no errors will be displayed. This is not a + technical limitation but rather intentional design. `panic` is not for error reporting. + +## MSRV policy + +The MSRV is currently `1.61`, and this is considered a breaking change to increase. + +However, if an existing dependency requires a higher MSRV without a semver breaking update, this may be raised. + +## Motivation + +Error handling in proc-macros sucks. There's not much of a choice today: +you either "bubble up" the error up to the top-level of the macro and convert it to +a [`compile_error!`][compl_err] invocation or just use a good old panic. Both these ways suck: + +- Former sucks because it's quite redundant to unroll a proper error handling + just for critical errors that will crash the macro anyway; so people mostly + choose not to bother with it at all and use panic. Simple `.expect` is too tempting. + + Also, if you do decide to implement this `Result`-based architecture in your macro + you're going to have to rewrite it entirely once [`proc_macro::Diagnostic`][] is finally + stable. Not cool. + +- Later sucks because there's no way to carry out the span info via `panic!`. + `rustc` will highlight the invocation itself but not some specific token inside it. + + Furthermore, panics aren't for error-reporting at all; panics are for bug-detecting + (like unwrapping on `None` or out-of-range indexing) or for early development stages + when you need a prototype ASAP so error handling can wait. Mixing these usages only + messes things up. + +- There is [`proc_macro::Diagnostic`][] which is awesome but it has been experimental + for more than a year and is unlikely to be stabilized any time soon. + + This crate's API is intentionally designed to be compatible with `proc_macro::Diagnostic` + and delegates to it whenever possible. Once `Diagnostics` is stable this crate + will **always** delegate to it, no code changes will be required on user side. + +That said, we need a solution, but this solution must meet these conditions: + +- It must be better than `panic!`. The main point: it must offer a way to carry the span information + over to user. +- It must take as little effort as possible to migrate from `panic!`. Ideally, a new + macro with similar semantics plus ability to carry out span info. +- It must maintain compatibility with [`proc_macro::Diagnostic`][] . +- **It must be usable on stable**. + +This crate aims to provide such a mechanism. All you have to do is annotate your top-level +`#[proc_macro]` function with `#[proc_macro_error]` attribute and change panics to +[`abort!`]/[`abort_call_site!`] where appropriate, see [the Guide][guide]. + +## Disclaimer +Please note that **this crate is not intended to be used in any way other +than error reporting in procedural macros**, use `Result` and `?` (possibly along with one of the +many helpers out there) for anything else. + +
+ +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + + + +[compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html +[`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html + +[crate::dummy]: https://docs.rs/proc-macro-error2/1/proc_macro_error/dummy/index.html +[crate::multi]: https://docs.rs/proc-macro-error2/1/proc_macro_error/multi/index.html + +[`abort_call_site!`]: https://docs.rs/proc-macro-error2/1/proc_macro_error/macro.abort_call_site.html +[`abort!`]: https://docs.rs/proc-macro-error2/1/proc_macro_error/macro.abort.html +[guide]: https://docs.rs/proc-macro-error2 diff --git a/third-party/proc-macro-error2/src/diagnostic.rs b/third-party/proc-macro-error2/src/diagnostic.rs new file mode 100644 index 0000000..0455511 --- /dev/null +++ b/third-party/proc-macro-error2/src/diagnostic.rs @@ -0,0 +1,360 @@ +use crate::{abort_now, check_correctness, sealed::Sealed, SpanRange}; +use proc_macro2::Span; +use proc_macro2::TokenStream; + +use quote::{quote_spanned, ToTokens}; + +/// Represents a diagnostic level +/// +/// # Warnings +/// +/// Warnings are ignored on stable/beta +#[derive(Debug, PartialEq)] +#[non_exhaustive] +pub enum Level { + Error, + Warning, +} + +/// Represents a single diagnostic message +#[derive(Debug)] +#[must_use = "A diagnostic does nothing unless emitted"] +pub struct Diagnostic { + pub(crate) level: Level, + pub(crate) span_range: SpanRange, + pub(crate) msg: String, + pub(crate) suggestions: Vec<(SuggestionKind, String, Option)>, + pub(crate) children: Vec<(SpanRange, String)>, +} + +/// A collection of methods that do not exist in `proc_macro::Diagnostic` +/// but still useful to have around. +/// +/// This trait is sealed and cannot be implemented outside of `proc_macro_error`. +pub trait DiagnosticExt: Sealed { + /// Create a new diagnostic message that points to the `span_range`. + /// + /// This function is the same as `Diagnostic::spanned` but produces considerably + /// better error messages for multi-token spans on stable. + fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self; + + /// Add another error message to self such that it will be emitted right after + /// the main message. + /// + /// This function is the same as `Diagnostic::span_error` but produces considerably + /// better error messages for multi-token spans on stable. + #[must_use] + fn span_range_error(self, span_range: SpanRange, msg: String) -> Self; + + /// Attach a "help" note to your main message, the note will have it's own span on nightly. + /// + /// This function is the same as `Diagnostic::span_help` but produces considerably + /// better error messages for multi-token spans on stable. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + #[must_use] + fn span_range_help(self, span_range: SpanRange, msg: String) -> Self; + + /// Attach a note to your main message, the note will have it's own span on nightly. + /// + /// This function is the same as `Diagnostic::span_note` but produces considerably + /// better error messages for multi-token spans on stable. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + #[must_use] + fn span_range_note(self, span_range: SpanRange, msg: String) -> Self; +} + +impl DiagnosticExt for Diagnostic { + fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self { + Diagnostic { + level, + span_range, + msg: message, + suggestions: vec![], + children: vec![], + } + } + + fn span_range_error(mut self, span_range: SpanRange, msg: String) -> Self { + self.children.push((span_range, msg)); + self + } + + fn span_range_help(mut self, span_range: SpanRange, msg: String) -> Self { + self.suggestions + .push((SuggestionKind::Help, msg, Some(span_range))); + self + } + + fn span_range_note(mut self, span_range: SpanRange, msg: String) -> Self { + self.suggestions + .push((SuggestionKind::Note, msg, Some(span_range))); + self + } +} + +impl Diagnostic { + /// Create a new diagnostic message that points to `Span::call_site()` + pub fn new(level: Level, message: String) -> Self { + Diagnostic::spanned(Span::call_site(), level, message) + } + + /// Create a new diagnostic message that points to the `span` + pub fn spanned(span: Span, level: Level, message: String) -> Self { + Diagnostic::spanned_range( + SpanRange { + first: span, + last: span, + }, + level, + message, + ) + } + + /// Add another error message to self such that it will be emitted right after + /// the main message. + pub fn span_error(self, span: Span, msg: String) -> Self { + self.span_range_error( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a "help" note to your main message, the note will have it's own span on nightly. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + pub fn span_help(self, span: Span, msg: String) -> Self { + self.span_range_help( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a "help" note to your main message. + pub fn help(mut self, msg: String) -> Self { + self.suggestions.push((SuggestionKind::Help, msg, None)); + self + } + + /// Attach a note to your main message, the note will have it's own span on nightly. + /// + /// # Span + /// + /// The span is ignored on stable, the note effectively inherits its parent's (main message) span + pub fn span_note(self, span: Span, msg: String) -> Self { + self.span_range_note( + SpanRange { + first: span, + last: span, + }, + msg, + ) + } + + /// Attach a note to your main message + pub fn note(mut self, msg: String) -> Self { + self.suggestions.push((SuggestionKind::Note, msg, None)); + self + } + + /// The message of main warning/error (no notes attached) + #[must_use] + pub fn message(&self) -> &str { + &self.msg + } + + /// Abort the proc-macro's execution and display the diagnostic. + /// + /// # Warnings + /// + /// Warnings are not emitted on stable and beta, but this function will abort anyway. + pub fn abort(self) -> ! { + self.emit(); + abort_now() + } + + /// Display the diagnostic while not aborting macro execution. + /// + /// # Warnings + /// + /// Warnings are ignored on stable/beta + pub fn emit(self) { + check_correctness(); + crate::imp::emit_diagnostic(self); + } +} + +/// **NOT PUBLIC API! NOTHING TO SEE HERE!!!** +#[doc(hidden)] +impl Diagnostic { + pub fn span_suggestion(self, span: Span, suggestion: &str, msg: String) -> Self { + match suggestion { + "help" | "hint" => self.span_help(span, msg), + _ => self.span_note(span, msg), + } + } + + pub fn suggestion(self, suggestion: &str, msg: String) -> Self { + match suggestion { + "help" | "hint" => self.help(msg), + _ => self.note(msg), + } + } +} + +impl ToTokens for Diagnostic { + fn to_tokens(&self, ts: &mut TokenStream) { + use std::borrow::Cow; + + fn ensure_lf(buf: &mut String, s: &str) { + if s.ends_with('\n') { + buf.push_str(s); + } else { + buf.push_str(s); + buf.push('\n'); + } + } + + fn diag_to_tokens( + span_range: SpanRange, + level: &Level, + msg: &str, + suggestions: &[(SuggestionKind, String, Option)], + ) -> TokenStream { + if *level == Level::Warning { + return TokenStream::new(); + } + + let message = if suggestions.is_empty() { + Cow::Borrowed(msg) + } else { + let mut message = String::new(); + ensure_lf(&mut message, msg); + message.push('\n'); + + for (kind, note, _span) in suggestions { + message.push_str(" = "); + message.push_str(kind.name()); + message.push_str(": "); + ensure_lf(&mut message, note); + } + message.push('\n'); + + Cow::Owned(message) + }; + + let mut msg = proc_macro2::Literal::string(&message); + msg.set_span(span_range.last); + let group = quote_spanned!(span_range.last=> { #msg } ); + quote_spanned!(span_range.first=> compile_error!#group) + } + + ts.extend(diag_to_tokens( + self.span_range, + &self.level, + &self.msg, + &self.suggestions, + )); + ts.extend( + self.children + .iter() + .map(|(span_range, msg)| diag_to_tokens(*span_range, &Level::Error, msg, &[])), + ); + } +} + +#[derive(Debug)] +pub(crate) enum SuggestionKind { + Help, + Note, +} + +impl SuggestionKind { + fn name(&self) -> &'static str { + match self { + SuggestionKind::Note => "note", + SuggestionKind::Help => "help", + } + } +} + +#[cfg(feature = "syn-error")] +impl From for Diagnostic { + fn from(err: syn::Error) -> Self { + use proc_macro2::{Delimiter, TokenTree}; + + fn gut_error(ts: &mut impl Iterator) -> Option<(SpanRange, String)> { + let start_span = ts.next()?.span(); + ts.next().expect(":1"); + ts.next().expect("core"); + ts.next().expect(":2"); + ts.next().expect(":3"); + ts.next().expect("compile_error"); + ts.next().expect("!"); + + let lit = match ts.next().unwrap() { + TokenTree::Group(group) => { + // Currently `syn` builds `compile_error!` invocations + // exclusively in `ident{"..."}` (braced) form which is not + // followed by `;` (semicolon). + // + // But if it changes to `ident("...");` (parenthesized) + // or `ident["..."];` (bracketed) form, + // we will need to skip the `;` as well. + // Highly unlikely, but better safe than sorry. + + if group.delimiter() == Delimiter::Parenthesis + || group.delimiter() == Delimiter::Bracket + { + ts.next().unwrap(); // ; + } + + match group.stream().into_iter().next().unwrap() { + TokenTree::Literal(lit) => lit, + _ => unreachable!(""), + } + } + _ => unreachable!(""), + }; + + let last = lit.span(); + let mut msg = lit.to_string(); + + // "abc" => abc + msg.pop(); + msg.remove(0); + + Some(( + SpanRange { + first: start_span, + last, + }, + msg, + )) + } + + let mut ts = err.to_compile_error().into_iter(); + + let (span_range, msg) = gut_error(&mut ts).unwrap(); + let mut res = Diagnostic::spanned_range(span_range, Level::Error, msg); + + while let Some((span_range, msg)) = gut_error(&mut ts) { + res = res.span_range_error(span_range, msg); + } + + res + } +} diff --git a/third-party/proc-macro-error2/src/dummy.rs b/third-party/proc-macro-error2/src/dummy.rs new file mode 100644 index 0000000..5bc98bd --- /dev/null +++ b/third-party/proc-macro-error2/src/dummy.rs @@ -0,0 +1,151 @@ +//! Facility to emit dummy implementations (or whatever) in case +//! an error happen. +//! +//! `compile_error!` does not abort a compilation right away. This means +//! `rustc` doesn't just show you the error and abort, it carries on the +//! compilation process looking for other errors to report. +//! +//! Let's consider an example: +//! +//! ```rust,ignore +//! use proc_macro::TokenStream; +//! use proc_macro_error2::*; +//! +//! trait MyTrait { +//! fn do_thing(); +//! } +//! +//! // this proc macro is supposed to generate MyTrait impl +//! #[proc_macro_derive(MyTrait)] +//! #[proc_macro_error] +//! fn example(input: TokenStream) -> TokenStream { +//! // somewhere deep inside +//! abort!(span, "something's wrong"); +//! +//! // this implementation will be generated if no error happened +//! quote! { +//! impl MyTrait for #name { +//! fn do_thing() {/* whatever */} +//! } +//! } +//! } +//! +//! // ================ +//! // in main.rs +//! +//! // this derive triggers an error +//! #[derive(MyTrait)] // first BOOM! +//! struct Foo; +//! +//! fn main() { +//! Foo::do_thing(); // second BOOM! +//! } +//! ``` +//! +//! The problem is: the generated token stream contains only `compile_error!` +//! invocation, the impl was not generated. That means user will see two compilation +//! errors: +//! +//! ```text +//! error: something's wrong +//! --> $DIR/probe.rs:9:10 +//! | +//! 9 |#[proc_macro_derive(MyTrait)] +//! | ^^^^^^^ +//! +//! error[E0599]: no function or associated item named `do_thing` found for type `Foo` in the current scope +//! --> src\main.rs:3:10 +//! | +//! 1 | struct Foo; +//! | ----------- function or associated item `do_thing` not found for this +//! 2 | fn main() { +//! 3 | Foo::do_thing(); // second BOOM! +//! | ^^^^^^^^ function or associated item not found in `Foo` +//! ``` +//! +//! But the second error is meaningless! We definitely need to fix this. +//! +//! Most used approach in cases like this is "dummy implementation" - +//! omit `impl MyTrait for #name` and fill functions bodies with `unimplemented!()`. +//! +//! This is how you do it: +//! +//! ```rust,ignore +//! use proc_macro::TokenStream; +//! use proc_macro_error2::*; +//! +//! trait MyTrait { +//! fn do_thing(); +//! } +//! +//! // this proc macro is supposed to generate MyTrait impl +//! #[proc_macro_derive(MyTrait)] +//! #[proc_macro_error] +//! fn example(input: TokenStream) -> TokenStream { +//! // first of all - we set a dummy impl which will be appended to +//! // `compile_error!` invocations in case a trigger does happen +//! set_dummy(quote! { +//! impl MyTrait for #name { +//! fn do_thing() { unimplemented!() } +//! } +//! }); +//! +//! // somewhere deep inside +//! abort!(span, "something's wrong"); +//! +//! // this implementation will be generated if no error happened +//! quote! { +//! impl MyTrait for #name { +//! fn do_thing() {/* whatever */} +//! } +//! } +//! } +//! +//! // ================ +//! // in main.rs +//! +//! // this derive triggers an error +//! #[derive(MyTrait)] // first BOOM! +//! struct Foo; +//! +//! fn main() { +//! Foo::do_thing(); // no more errors! +//! } +//! ``` + +use proc_macro2::TokenStream; +use std::cell::RefCell; + +use crate::check_correctness; + +thread_local! { + static DUMMY_IMPL: RefCell> = const { RefCell::new(None) }; +} + +/// Sets dummy token stream which will be appended to `compile_error!(msg);...` +/// invocations in case you'll emit any errors. +/// +/// See [guide](../index.html#guide). +#[allow(clippy::must_use_candidate)] // Mutates thread local state +pub fn set_dummy(dummy: TokenStream) -> Option { + check_correctness(); + DUMMY_IMPL.with(|old_dummy| old_dummy.replace(Some(dummy))) +} + +/// Same as [`set_dummy`] but, instead of resetting, appends tokens to the +/// existing dummy (if any). Behaves as `set_dummy` if no dummy is present. +pub fn append_dummy(dummy: TokenStream) { + check_correctness(); + DUMMY_IMPL.with(|old_dummy| { + let mut cell = old_dummy.borrow_mut(); + if let Some(ts) = cell.as_mut() { + ts.extend(dummy); + } else { + *cell = Some(dummy); + } + }); +} + +pub(crate) fn cleanup() -> Option { + DUMMY_IMPL.with(|old_dummy| old_dummy.replace(None)) +} diff --git a/third-party/proc-macro-error2/src/imp/delegate.rs b/third-party/proc-macro-error2/src/imp/delegate.rs new file mode 100644 index 0000000..a3e6a80 --- /dev/null +++ b/third-party/proc-macro-error2/src/imp/delegate.rs @@ -0,0 +1,68 @@ +//! This implementation uses [`proc_macro::Diagnostic`], nightly only. + +use std::cell::Cell; + +use proc_macro::{Diagnostic as PDiag, Level as PLevel}; + +use crate::{ + abort_now, check_correctness, + diagnostic::{Diagnostic, Level, SuggestionKind}, +}; + +pub fn abort_if_dirty() { + check_correctness(); + if IS_DIRTY.with(|c| c.get()) { + abort_now() + } +} + +pub(crate) fn cleanup() -> Vec { + IS_DIRTY.with(|c| c.set(false)); + vec![] +} + +pub(crate) fn emit_diagnostic(diag: Diagnostic) { + let Diagnostic { + level, + span_range, + msg, + suggestions, + children, + } = diag; + + let span = span_range.collapse().unwrap(); + + let level = match level { + Level::Warning => PLevel::Warning, + Level::Error => { + IS_DIRTY.with(|c| c.set(true)); + PLevel::Error + } + }; + + let mut res = PDiag::spanned(span, level, msg); + + for (kind, msg, span) in suggestions { + res = match (kind, span) { + (SuggestionKind::Note, Some(span_range)) => { + res.span_note(span_range.collapse().unwrap(), msg) + } + (SuggestionKind::Help, Some(span_range)) => { + res.span_help(span_range.collapse().unwrap(), msg) + } + (SuggestionKind::Note, None) => res.note(msg), + (SuggestionKind::Help, None) => res.help(msg), + } + } + + for (span_range, msg) in children { + let span = span_range.collapse().unwrap(); + res = res.span_error(span, msg); + } + + res.emit() +} + +thread_local! { + static IS_DIRTY: Cell = Cell::new(false); +} diff --git a/third-party/proc-macro-error2/src/imp/fallback.rs b/third-party/proc-macro-error2/src/imp/fallback.rs new file mode 100644 index 0000000..c10eb9a --- /dev/null +++ b/third-party/proc-macro-error2/src/imp/fallback.rs @@ -0,0 +1,30 @@ +//! This implementation uses self-written stable facilities. + +use crate::{ + abort_now, check_correctness, + diagnostic::{Diagnostic, Level}, +}; +use std::cell::RefCell; + +pub fn abort_if_dirty() { + check_correctness(); + ERR_STORAGE.with(|storage| { + if !storage.borrow().is_empty() { + abort_now() + } + }); +} + +pub(crate) fn cleanup() -> Vec { + ERR_STORAGE.with(|storage| storage.replace(Vec::new())) +} + +pub(crate) fn emit_diagnostic(diag: Diagnostic) { + if diag.level == Level::Error { + ERR_STORAGE.with(|storage| storage.borrow_mut().push(diag)); + } +} + +thread_local! { + static ERR_STORAGE: RefCell> = const { RefCell::new(Vec::new()) }; +} diff --git a/third-party/proc-macro-error2/src/lib.rs b/third-party/proc-macro-error2/src/lib.rs new file mode 100644 index 0000000..d496790 --- /dev/null +++ b/third-party/proc-macro-error2/src/lib.rs @@ -0,0 +1,565 @@ +//! # proc-macro-error2 +//! +//! This crate aims to make error reporting in proc-macros simple and easy to use. +//! Migrate from `panic!`-based errors for as little effort as possible! +//! +//! (Also, you can explicitly [append a dummy token stream](dummy/index.html) to your errors). +//! +//! To achieve his, this crate serves as a tiny shim around `proc_macro::Diagnostic` and +//! `compile_error!`. It detects the best way of emitting available based on compiler's version. +//! When the underlying diagnostic type is finally stabilized, this crate will simply be +//! delegating to it requiring no changes in your code! +//! +//! So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality +//! available on stable ahead of time *and* your error-reporting code future-proof. +//! +//! ## Cargo features +//! +//! This crate provides *enabled by default* `syn-error` feature that gates +//! `impl From for Diagnostic` conversion. If you don't use `syn` and want +//! to cut off some of compilation time, you can disable it via +//! +//! ```toml +//! [dependencies] +//! proc-macro-error2 = { version = "2.0.0", default-features = false } +//! ``` +//! +//! ***Please note that disabling this feature makes sense only if you don't depend on `syn` +//! directly or indirectly, and you very likely do.** +//! +//! ## Real world examples +//! +//! * [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive) +//! (abort-like usage) +//! * [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage) +//! +//! ## Limitations +//! +//! - Warnings are emitted only on nightly, they are ignored on stable. +//! - "help" suggestions can't have their own span info on stable, +//! (essentially inheriting the parent span). +//! - If a panic occurs somewhere in your macro no errors will be displayed. This is not a +//! technical limitation but rather intentional design. `panic` is not for error reporting. +//! +//! ### `#[proc_macro_error]` attribute +//! +//! **This attribute MUST be present on the top level of your macro** (the function +//! annotated with any of `#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`). +//! +//! This attribute performs the setup and cleanup necessary to make things work. +//! +//! In most cases you'll need the simple `#[proc_macro_error]` form without any +//! additional settings. Feel free to [skip the "Syntax" section](#macros). +//! +//! #### Syntax +//! +//! `#[proc_macro_error]` or `#[proc_macro_error(settings...)]`, where `settings...` +//! is a comma-separated list of: +//! +//! - `proc_macro_hack`: +//! +//! In order to correctly cooperate with `#[proc_macro_hack]`, `#[proc_macro_error]` +//! attribute must be placed *before* (above) it, like this: +//! +//! ```no_run +//! # use proc_macro2::TokenStream; +//! # const IGNORE: &str = " +//! #[proc_macro_error] +//! #[proc_macro_hack] +//! #[proc_macro] +//! # "; +//! fn my_macro(input: TokenStream) -> TokenStream { +//! unimplemented!() +//! } +//! ``` +//! +//! If, for some reason, you can't place it like that you can use +//! `#[proc_macro_error(proc_macro_hack)]` instead. +//! +//! # Note +//! +//! If `proc-macro-hack` was detected (by any means) `allow_not_macro` +//! and `assert_unwind_safe` will be applied automatically. +//! +//! - `allow_not_macro`: +//! +//! By default, the attribute checks that it's applied to a proc-macro. +//! If none of `#[proc_macro]`, `#[proc_macro_derive]` nor `#[proc_macro_attribute]` are +//! present it will panic. It's the intention - this crate is supposed to be used only with +//! proc-macros. +//! +//! This setting is made to bypass the check, useful in certain circumstances. +//! +//! Pay attention: the function this attribute is applied to must return +//! `proc_macro::TokenStream`. +//! +//! This setting is implied if `proc-macro-hack` was detected. +//! +//! - `assert_unwind_safe`: +//! +//! By default, your code must be [unwind safe]. If your code is not unwind safe, +//! but you believe it's correct, you can use this setting to bypass the check. +//! You would need this for code that uses `lazy_static` or `thread_local` with +//! `Cell/RefCell` inside (and the like). +//! +//! This setting is implied if `#[proc_macro_error]` is applied to a function +//! marked as `#[proc_macro]`, `#[proc_macro_derive]` or `#[proc_macro_attribute]`. +//! +//! This setting is also implied if `proc-macro-hack` was detected. +//! +//! ## Macros +//! +//! Most of the time you want to use the macros. Syntax is described in the next section below. +//! +//! You'll need to decide how you want to emit errors: +//! +//! * Emit the error and abort. Very much panic-like usage. Served by [`abort!`] and +//! [`abort_call_site!`]. +//! * Emit the error but do not abort right away, looking for other errors to report. +//! Served by [`emit_error!`] and [`emit_call_site_error!`]. +//! +//! You **can** mix these usages. +//! +//! `abort` and `emit_error` take a "source span" as the first argument. This source +//! will be used to highlight the place the error originates from. It must be one of: +//! +//! * *Something* that implements [`ToTokens`] (most types in `syn` and `proc-macro2` do). +//! This source is the preferable one since it doesn't lose span information on multi-token +//! spans, see [this issue](https://gitlab.com/CreepySkeleton/proc-macro-error/-/issues/6) +//! for details. +//! * [`proc_macro::Span`] +//! * [`proc-macro2::Span`] +//! +//! The rest is your message in format-like style. +//! +//! See [the next section](#syntax-1) for detailed syntax. +//! +//! - [`abort!`]: +//! +//! Very much panic-like usage - abort right away and show the error. +//! Expands to [`!`] (never type). +//! +//! - [`abort_call_site!`]: +//! +//! Shortcut for `abort!(Span::call_site(), ...)`. Expands to [`!`] (never type). +//! +//! - [`emit_error!`]: +//! +//! [`proc_macro::Diagnostic`]-like usage - emit the error but keep going, +//! looking for other errors to report. +//! The compilation will fail nonetheless. Expands to [`()`] (unit type). +//! +//! - [`emit_call_site_error!`]: +//! +//! Shortcut for `emit_error!(Span::call_site(), ...)`. Expands to [`()`] (unit type). +//! +//! - [`emit_warning!`]: +//! +//! Like `emit_error!` but emit a warning instead of error. The compilation won't fail +//! because of warnings. +//! Expands to [`()`] (unit type). +//! +//! **Beware**: warnings are nightly only, they are completely ignored on stable. +//! +//! - [`emit_call_site_warning!`]: +//! +//! Shortcut for `emit_warning!(Span::call_site(), ...)`. Expands to [`()`] (unit type). +//! +//! - [`diagnostic`]: +//! +//! Build an instance of `Diagnostic` in format-like style. +//! +//! #### Syntax +//! +//! All the macros have pretty much the same syntax: +//! +//! 1. ```ignore +//! abort!(single_expr) +//! ``` +//! Shortcut for `Diagnostic::from(expr).abort()`. +//! +//! 2. ```ignore +//! abort!(span, message) +//! ``` +//! The first argument is an expression the span info should be taken from. +//! +//! The second argument is the error message, it must implement [`ToString`]. +//! +//! 3. ```ignore +//! abort!(span, format_literal, format_args...) +//! ``` +//! +//! This form is pretty much the same as 2, except `format!(format_literal, format_args...)` +//! will be used to for the message instead of [`ToString`]. +//! +//! That's it. `abort!`, `emit_warning`, `emit_error` share this exact syntax. +//! +//! `abort_call_site!`, `emit_call_site_warning`, `emit_call_site_error` lack 1 form +//! and do not take span in 2'th and 3'th forms. Those are essentially shortcuts for +//! `macro!(Span::call_site(), args...)`. +//! +//! `diagnostic!` requires a [`Level`] instance between `span` and second argument +//! (1'th form is the same). +//! +//! > **Important!** +//! > +//! > If you have some type from `proc_macro` or `syn` to point to, do not call `.span()` +//! > on it but rather use it directly: +//! > ```no_run +//! > # use proc_macro_error2::abort; +//! > # let input = proc_macro2::TokenStream::new(); +//! > let ty: syn::Type = syn::parse2(input).unwrap(); +//! > abort!(ty, "BOOM"); +//! > // ^^ <-- avoid .span() +//! > ``` +//! > +//! > `.span()` calls work too, but you may experience regressions in message quality. +//! +//! #### Note attachments +//! +//! 3. Every macro can have "note" attachments (only 2 and 3 form). +//! ```ignore +//! let opt_help = if have_some_info { Some("did you mean `this`?") } else { None }; +//! +//! abort!( +//! span, message; // <--- attachments start with `;` (semicolon) +//! +//! help = "format {} {}", "arg1", "arg2"; // <--- every attachment ends with `;`, +//! // maybe except the last one +//! +//! note = "to_string"; // <--- one arg uses `.to_string()` instead of `format!()` +//! +//! yay = "I see what {} did here", "you"; // <--- "help =" and "hint =" are mapped +//! // to Diagnostic::help, +//! // anything else is Diagnostic::note +//! +//! wow = note_span => "custom span"; // <--- attachments can have their own span +//! // it takes effect only on nightly though +//! +//! hint =? opt_help; // <-- "optional" attachment, get displayed only if `Some` +//! // must be single `Option` expression +//! +//! note =? note_span => opt_help // <-- optional attachments can have custom spans too +//! ); +//! ``` +//! + +//! ### Diagnostic type +//! +//! [`Diagnostic`] type is intentionally designed to be API compatible with [`proc_macro::Diagnostic`]. +//! Not all API is implemented, only the part that can be reasonably implemented on stable. +//! +//! +//! [`abort!`]: macro.abort.html +//! [`abort_call_site!`]: macro.abort_call_site.html +//! [`emit_warning!`]: macro.emit_warning.html +//! [`emit_error!`]: macro.emit_error.html +//! [`emit_call_site_warning!`]: macro.emit_call_site_error.html +//! [`emit_call_site_error!`]: macro.emit_call_site_warning.html +//! [`diagnostic!`]: macro.diagnostic.html +//! [`Diagnostic`]: struct.Diagnostic.html +//! +//! [`proc_macro::Span`]: https://doc.rust-lang.org/proc_macro/struct.Span.html +//! [`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html +//! +//! [unwind safe]: https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html#what-is-unwind-safety +//! [`!`]: https://doc.rust-lang.org/std/primitive.never.html +//! [`()`]: https://doc.rust-lang.org/std/primitive.unit.html +//! [`ToString`]: https://doc.rust-lang.org/std/string/trait.ToString.html +//! +//! [`proc-macro2::Span`]: https://docs.rs/proc-macro2/1.0.10/proc_macro2/struct.Span.html +//! [`ToTokens`]: https://docs.rs/quote/1.0.3/quote/trait.ToTokens.html +//! + +#![cfg_attr(feature = "nightly", feature(proc_macro_diagnostic))] +#![forbid(unsafe_code)] + +pub extern crate proc_macro; + +pub use crate::{ + diagnostic::{Diagnostic, DiagnosticExt, Level}, + dummy::{append_dummy, set_dummy}, +}; +pub use proc_macro_error_attr2::proc_macro_error; + +use proc_macro2::Span; +use quote::{quote, ToTokens}; + +use std::cell::Cell; +use std::panic::{catch_unwind, resume_unwind, UnwindSafe}; + +pub mod dummy; + +mod diagnostic; +mod macros; +mod sealed; + +#[cfg(not(feature = "nightly"))] +#[path = "imp/fallback.rs"] +mod imp; + +#[cfg(feature = "nightly")] +#[path = "imp/delegate.rs"] +mod imp; + +#[derive(Debug, Clone, Copy)] +#[must_use = "A SpanRange does nothing unless used"] +pub struct SpanRange { + pub first: Span, + pub last: Span, +} + +impl SpanRange { + /// Create a range with the `first` and `last` spans being the same. + pub fn single_span(span: Span) -> Self { + SpanRange { + first: span, + last: span, + } + } + + /// Create a `SpanRange` resolving at call site. + pub fn call_site() -> Self { + SpanRange::single_span(Span::call_site()) + } + + /// Construct span range from a `TokenStream`. This method always preserves all the + /// range. + /// + /// ### Note + /// + /// If the stream is empty, the result is `SpanRange::call_site()`. If the stream + /// consists of only one `TokenTree`, the result is `SpanRange::single_span(tt.span())` + /// that doesn't lose anything. + pub fn from_tokens(ts: &dyn ToTokens) -> Self { + let mut spans = ts.to_token_stream().into_iter().map(|tt| tt.span()); + let first = spans.next().unwrap_or_else(Span::call_site); + let last = spans.last().unwrap_or(first); + + SpanRange { first, last } + } + + /// Join two span ranges. The resulting range will start at `self.first` and end at + /// `other.last`. + pub fn join_range(self, other: SpanRange) -> Self { + SpanRange { + first: self.first, + last: other.last, + } + } + + /// Collapse the range into single span, preserving as much information as possible. + #[must_use] + pub fn collapse(self) -> Span { + self.first.join(self.last).unwrap_or(self.first) + } +} + +/// This traits expands `Result>` with some handy shortcuts. +pub trait ResultExt { + type Ok; + + /// Behaves like `Result::unwrap`: if self is `Ok` yield the contained value, + /// otherwise abort macro execution via `abort!`. + fn unwrap_or_abort(self) -> Self::Ok; + + /// Behaves like `Result::expect`: if self is `Ok` yield the contained value, + /// otherwise abort macro execution via `abort!`. + /// If it aborts then resulting error message will be preceded with `message`. + fn expect_or_abort(self, msg: &str) -> Self::Ok; +} + +/// This traits expands `Option` with some handy shortcuts. +pub trait OptionExt { + type Some; + + /// Behaves like `Option::expect`: if self is `Some` yield the contained value, + /// otherwise abort macro execution via `abort_call_site!`. + /// If it aborts the `message` will be used for [`compile_error!`][compl_err] invocation. + /// + /// [compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html + fn expect_or_abort(self, msg: &str) -> Self::Some; +} + +/// Abort macro execution and display all the emitted errors, if any. +/// +/// Does nothing if no errors were emitted (warnings do not count). +pub fn abort_if_dirty() { + imp::abort_if_dirty(); +} + +impl> ResultExt for Result { + type Ok = T; + + fn unwrap_or_abort(self) -> T { + match self { + Ok(res) => res, + Err(e) => e.into().abort(), + } + } + + fn expect_or_abort(self, message: &str) -> T { + match self { + Ok(res) => res, + Err(e) => { + let mut e = e.into(); + e.msg = format!("{}: {}", message, e.msg); + e.abort() + } + } + } +} + +impl OptionExt for Option { + type Some = T; + + fn expect_or_abort(self, message: &str) -> T { + match self { + Some(res) => res, + None => abort_call_site!(message), + } + } +} + +/// This is the entry point for a proc-macro. +/// +/// **NOT PUBLIC API, SUBJECT TO CHANGE WITHOUT ANY NOTICE** +#[doc(hidden)] +pub fn entry_point(f: F, proc_macro_hack: bool) -> proc_macro::TokenStream +where + F: FnOnce() -> proc_macro::TokenStream + UnwindSafe, +{ + ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() + 1)); + let caught = catch_unwind(f); + let dummy = dummy::cleanup(); + let err_storage = imp::cleanup(); + ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() - 1)); + + let gen_error = || { + if proc_macro_hack { + quote! {{ + macro_rules! proc_macro_call { + () => ( unimplemented!() ) + } + + #(#err_storage)* + #dummy + + unimplemented!() + }} + } else { + quote!( #(#err_storage)* #dummy ) + } + }; + + match caught { + Ok(ts) => { + if err_storage.is_empty() { + ts + } else { + gen_error().into() + } + } + + Err(boxed) => match boxed.downcast::() { + Ok(_) => gen_error().into(), + Err(boxed) => resume_unwind(boxed), + }, + } +} + +fn abort_now() -> ! { + check_correctness(); + std::panic::panic_any(AbortNow) +} + +thread_local! { + static ENTERED_ENTRY_POINT: Cell = const { Cell::new(0) }; +} + +struct AbortNow; + +fn check_correctness() { + assert!( + ENTERED_ENTRY_POINT.with(Cell::get) != 0, + "proc-macro-error2 API cannot be used outside of `entry_point` invocation, \ + perhaps you forgot to annotate your #[proc_macro] function with `#[proc_macro_error]" + ); +} + +/// **ALL THE STUFF INSIDE IS NOT PUBLIC API!!!** +#[doc(hidden)] +pub mod __export { + // reexports for use in macros + pub use proc_macro; + pub use proc_macro2; + + use proc_macro2::Span; + use quote::ToTokens; + + use crate::SpanRange; + + // inspired by + // https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md#simple-application + + pub trait SpanAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait Span2AsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait ToTokensAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + pub trait SpanRangeAsSpanRange { + #[allow(non_snake_case)] + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange; + } + + impl ToTokensAsSpanRange for &T { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + let mut ts = self.to_token_stream().into_iter(); + let first = match ts.next() { + Some(t) => t.span(), + None => Span::call_site(), + }; + + let last = match ts.last() { + Some(t) => t.span(), + None => first, + }; + + SpanRange { first, last } + } + } + + impl Span2AsSpanRange for Span { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + SpanRange { + first: *self, + last: *self, + } + } + } + + impl SpanAsSpanRange for proc_macro::Span { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + SpanRange { + first: (*self).into(), + last: (*self).into(), + } + } + } + + impl SpanRangeAsSpanRange for SpanRange { + fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange { + *self + } + } +} diff --git a/third-party/proc-macro-error2/src/macros.rs b/third-party/proc-macro-error2/src/macros.rs new file mode 100644 index 0000000..747b684 --- /dev/null +++ b/third-party/proc-macro-error2/src/macros.rs @@ -0,0 +1,288 @@ +// FIXME: this can be greatly simplified via $()? +// as soon as MRSV hits 1.32 + +/// Build [`Diagnostic`](struct.Diagnostic.html) instance from provided arguments. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! diagnostic { + // from alias + ($err:expr) => { $crate::Diagnostic::from($err) }; + + // span, message, help + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+ ; $($rest:tt)+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + let diag = $crate::Diagnostic::spanned_range( + span_range, + $level, + format!($fmt, $($args),*) + ); + $crate::__pme__suggestions!(diag $($rest)*); + diag + }}; + + ($span:expr, $level:expr, $msg:expr ; $($rest:tt)+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + let diag = $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()); + $crate::__pme__suggestions!(diag $($rest)*); + diag + }}; + + // span, message, no help + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + $crate::Diagnostic::spanned_range( + span_range, + $level, + format!($fmt, $($args),*) + ) + }}; + + ($span:expr, $level:expr, $msg:expr) => {{ + #[allow(unused_imports)] + use $crate::__export::{ + ToTokensAsSpanRange, + Span2AsSpanRange, + SpanAsSpanRange, + SpanRangeAsSpanRange + }; + use $crate::DiagnosticExt; + let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(); + + $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string()) + }}; + + + // trailing commas + + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+, ; $($rest:tt)+) => { + $crate::diagnostic!($span, $level, $fmt, $($args),* ; $($rest)*) + }; + ($span:expr, $level:expr, $msg:expr, ; $($rest:tt)+) => { + $crate::diagnostic!($span, $level, $msg ; $($rest)*) + }; + ($span:expr, $level:expr, $fmt:expr, $($args:expr),+,) => { + $crate::diagnostic!($span, $level, $fmt, $($args),*) + }; + ($span:expr, $level:expr, $msg:expr,) => { + $crate::diagnostic!($span, $level, $msg) + }; + // ($err:expr,) => { $crate::diagnostic!($err) }; +} + +/// Abort proc-macro execution right now and display the error. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +#[macro_export] +macro_rules! abort { + ($err:expr) => { + $crate::diagnostic!($err).abort() + }; + + ($span:expr, $($tts:tt)*) => { + $crate::diagnostic!($span, $crate::Level::Error, $($tts)*).abort() + }; +} + +/// Shortcut for `abort!(Span::call_site(), msg...)`. This macro +/// is still preferable over plain panic, panics are not for error reporting. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! abort_call_site { + ($($tts:tt)*) => { + $crate::abort!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }; +} + +/// Emit an error while not aborting the proc-macro right away. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_error { + ($err:expr) => { + $crate::diagnostic!($err).emit() + }; + + ($span:expr, $($tts:tt)*) => {{ + let level = $crate::Level::Error; + $crate::diagnostic!($span, level, $($tts)*).emit() + }}; +} + +/// Shortcut for `emit_error!(Span::call_site(), ...)`. This macro +/// is still preferable over plain panic, panics are not for error reporting.. +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_call_site_error { + ($($tts:tt)*) => { + $crate::emit_error!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }; +} + +/// Emit a warning. Warnings are not errors and compilation won't fail because of them. +/// +/// **Does nothing on stable** +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_warning { + ($span:expr, $($tts:tt)*) => { + $crate::diagnostic!($span, $crate::Level::Warning, $($tts)*).emit() + }; +} + +/// Shortcut for `emit_warning!(Span::call_site(), ...)`. +/// +/// **Does nothing on stable** +/// +/// # Syntax +/// +/// See [the guide](index.html#guide). +/// +#[macro_export] +macro_rules! emit_call_site_warning { + ($($tts:tt)*) => {{ + $crate::emit_warning!($crate::__export::proc_macro2::Span::call_site(), $($tts)*) + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __pme__suggestions { + ($var:ident) => (); + + ($var:ident $help:ident =? $msg:expr) => { + let $var = if let Some(msg) = $msg { + $var.suggestion(stringify!($help), msg.to_string()) + } else { + $var + }; + }; + ($var:ident $help:ident =? $span:expr => $msg:expr) => { + let $var = if let Some(msg) = $msg { + $var.span_suggestion($span.into(), stringify!($help), msg.to_string()) + } else { + $var + }; + }; + + ($var:ident $help:ident =? $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help =? $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident =? $span:expr => $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help =? $span => $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + + + ($var:ident $help:ident = $msg:expr) => { + let $var = $var.suggestion(stringify!($help), $msg.to_string()); + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+) => { + let $var = $var.suggestion( + stringify!($help), + format!($fmt, $($args),*) + ); + }; + ($var:ident $help:ident = $span:expr => $msg:expr) => { + let $var = $var.span_suggestion($span.into(), stringify!($help), $msg.to_string()); + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+) => { + let $var = $var.span_suggestion( + $span.into(), + stringify!($help), + format!($fmt, $($args),*) + ); + }; + + ($var:ident $help:ident = $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args),*); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $span:expr => $msg:expr ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $msg); + $crate::__pme__suggestions!($var $($rest)*); + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*); + $crate::__pme__suggestions!($var $($rest)*); + }; + + // trailing commas + + ($var:ident $help:ident = $msg:expr,) => { + $crate::__pme__suggestions!($var $help = $msg) + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+,) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args)*) + }; + ($var:ident $help:ident = $span:expr => $msg:expr,) => { + $crate::__pme__suggestions!($var $help = $span => $msg) + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),*,) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args)*) + }; + ($var:ident $help:ident = $msg:expr, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $msg; $($rest)*) + }; + ($var:ident $help:ident = $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $fmt, $($args),*; $($rest)*) + }; + ($var:ident $help:ident = $span:expr => $msg:expr, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $msg; $($rest)*) + }; + ($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => { + $crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*; $($rest)*) + }; +} diff --git a/third-party/proc-macro-error2/src/sealed.rs b/third-party/proc-macro-error2/src/sealed.rs new file mode 100644 index 0000000..a2d5081 --- /dev/null +++ b/third-party/proc-macro-error2/src/sealed.rs @@ -0,0 +1,3 @@ +pub trait Sealed {} + +impl Sealed for crate::Diagnostic {}