Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ rust-version = "1.85"
proc-macro2 = "1.0"
quote = "1.0"
serde = { version = "1.0", features = ["derive"] }
syn = { version = "3.0.2", features = ["full"] }
syn = { version = "3.0.5", features = ["full"] }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumped this up to pull in dtolnay/syn#2080.

28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,34 @@ wrapper around `syn::Parse` that implements `Deserialize`. The latter is useful
for passing in, for example, a `syn::Path`, or other specific entities from the
`syn` crate.

## String values with spans

A `String` field accepts either a string literal or a bare identifier, but the
deserialized `String` doesn't record where it came from. If your macro
interprets a string further (as an identifier, a file name, and so on),
deserialize it as a `ParseWrapper<SpannedString>` instead. It accepts the same
inputs and records the span of the token, so that errors about the value can
point at the value itself:

```rust
#[derive(Deserialize)]
struct Config {
module: ParseWrapper<SpannedString>,
}

let config = from_tokenstream::<Config>(&attr)?;
let module = config.module.parse::<syn::Ident>().map_err(|err| {
syn::Error::new(
config.module.span(),
format!("`{}` is not a valid module name: {err}", config.module.value()),
)
})?;
```

Given, for example, `module = "not a module"`, the error is reported at `"not a
module"`. Values parsed out of a `SpannedString` (via `parse` or `parse_with`)
also carry the span.

## OrderedMap

You may want to use the map syntax with keys that cannot be used by types such
Expand Down
160 changes: 159 additions & 1 deletion src/ibidem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

use std::cell::RefCell;

use proc_macro2::{TokenStream, TokenTree};
use proc_macro2::{Span, TokenStream, TokenTree};
use serde::{Deserialize, de::Error, de::Visitor};
use syn::ext::IdentExt;

use crate::serde_tokenstream::spanned_error;

Expand Down Expand Up @@ -141,6 +142,163 @@ impl<P: syn::parse::Parse> std::ops::Deref for ParseWrapper<P> {
}
}

/// A string value that remembers its span.
///
/// Use this as [`ParseWrapper`]`<SpannedString>`. It accepts the same inputs
/// as [`String`] (a string literal or a bare identifier), but also records the
/// span of that token. Macros that interpret a string further (as an
/// identifier, a file name, and so on) can use the span to report errors at
/// the value itself rather than at the attribute as a whole.
///
/// The [`parse`](Self::parse) and [`parse_with`](Self::parse_with) methods
/// parse the value as Rust syntax, similar to [`syn::LitStr::parse`]. The
/// resulting types and errors have the correct span information associated with
/// them.
///
/// Equality and hashing compare only the value, not the span.
///
/// # Example
///
/// ```
/// use quote::quote;
/// use serde::Deserialize;
/// use serde_tokenstream::{ParseWrapper, SpannedString, from_tokenstream};
///
/// #[derive(Deserialize)]
/// struct Config {
/// module: ParseWrapper<SpannedString>,
/// }
///
/// // In a proc macro, this would be the macro's input.
/// let attr = quote! { module = "not a module" };
/// let config = from_tokenstream::<Config>(&attr)?;
///
/// // Interpret the value further, reporting errors at the value rather than
/// // at the attribute as a whole.
/// let module = config.module.parse::<syn::Ident>().map_err(|err| {
/// syn::Error::new(
/// config.module.span(),
/// format!(
/// "`{}` is not a valid module name: {err}",
/// config.module.value()
/// ),
/// )
/// });
/// assert!(module.is_err());
/// # Ok::<(), syn::Error>(())
/// ```
///
/// # Limitations
///
/// See the [`ParseWrapper`] documentation for limitations.
///
/// [`from_tokenstream`]: crate::from_tokenstream
/// [`from_tokenstream_spanned`]: crate::from_tokenstream_spanned
#[derive(Debug, Clone)]
pub struct SpannedString {
value: String,
span: Span,
}

impl SpannedString {
/// Creates a `SpannedString` from a value and a span.
///
/// Deserializing a `ParseWrapper<SpannedString>` is the usual way to
/// obtain a `SpannedString` -- this is for cases like default values and
/// tests.
pub fn new(value: impl Into<String>, span: Span) -> Self {
Self { value: value.into(), span }
}

/// Returns the value.
///
/// In case of a string literal:
///
/// - The quotes are stripped from the value.
/// - Escapes are processed, so that (e.g.) `"\n"` becomes a newline.
///
/// Identifiers are stored verbatim, so raw identifiers keep the `r#` prefix.
pub fn value(&self) -> &str {
&self.value
}

/// Returns the span of the token the value was written as.
pub fn span(&self) -> Span {
self.span
}

/// Returns the value, discarding the span.
pub fn into_string(self) -> String {
self.value
}

/// Parses the value as a `T`.
///
/// In both success and error cases, the span points to this value.
pub fn parse<T: syn::parse::Parse>(&self) -> syn::Result<T> {
self.parse_with(T::parse)
}

/// Invokes `parser` on the value.
///
/// In both success and error cases, the span points to this value.
pub fn parse_with<F: syn::parse::Parser>(
&self,
parser: F,
) -> syn::Result<F::Output> {
syn::LitStr::new(&self.value, self.span).parse_with(parser)
}
}

impl AsRef<str> for SpannedString {
fn as_ref(&self) -> &str {
&self.value
}
}

impl std::fmt::Display for SpannedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.value)
}
}

impl PartialEq for SpannedString {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}

impl Eq for SpannedString {}

impl std::hash::Hash for SpannedString {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}

impl syn::parse::Parse for SpannedString {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let value = if input.peek(syn::LitStr) {
let lit: syn::LitStr = input.parse()?;
Self { value: lit.value(), span: lit.span() }
} else if input.peek(syn::Ident::peek_any) {
// Keywords are accepted, as they are for `String`.
let ident = syn::Ident::parse_any(input)?;
Self { value: ident.to_string(), span: ident.span() }
} else {
return Err(match input.cursor().token_tree() {
Some((tt, _)) => spanned_error(
&tt,
format!("expected a string, but found `{tt}`"),
),
None => input.error("expected a string"),
});
};

Ok(value)
}
}

/// We would like to be able to pass `TokenStream`s through unperturbed, but
/// that isn't directly possible with serde's model, because
/// serde--wisely--does not permit this kind of unholy communion between
Expand Down
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,19 @@
//!
//! For attributes that are nested inside a top-level macro, use the
//! [`from_tokenstream_spanned`] function. See its help for an example.
//!
//! ## Values with spans
//!
//! To report errors at a particular value rather than at the attribute as a
//! whole, deserialize it as a [`ParseWrapper`]: over a `syn` type for Rust
//! syntax, or over [`SpannedString`] for a plain string or identifier.

mod ibidem;
mod ordered_map;
mod serde_tokenstream;

pub use crate::ibidem::ParseWrapper;
pub use crate::ibidem::SpannedString;
pub use crate::ibidem::TokenStreamWrapper;
pub use crate::ordered_map::OrderedMap;
pub use crate::serde_tokenstream::Error;
Expand Down
92 changes: 91 additions & 1 deletion src/serde_tokenstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1344,7 +1344,7 @@ impl<'de> Deserializer<'de> for &mut TokenDe {

#[cfg(test)]
mod tests {
use crate::{ParseWrapper, ibidem::TokenStreamWrapper};
use crate::{ParseWrapper, SpannedString, ibidem::TokenStreamWrapper};

use super::*;
use quote::{ToTokens, quote};
Expand Down Expand Up @@ -2101,6 +2101,96 @@ mod tests {
assert_eq!(d.thing, Thing::D { d: "d".to_string() });
}

#[test]
fn test_spanned_string() {
#[derive(Deserialize)]
struct Stuff {
lit: ParseWrapper<SpannedString>,
ident: ParseWrapper<SpannedString>,
keyword: ParseWrapper<SpannedString>,
raw: ParseWrapper<SpannedString>,
missing: Option<ParseWrapper<SpannedString>>,
present: Option<ParseWrapper<SpannedString>>,
many: Vec<ParseWrapper<SpannedString>>,
}

let Stuff { lit, ident, keyword, raw, missing, present, many } =
from_tokenstream::<Stuff>(&quote! {
lit = "howdy",
ident = word,
keyword = mod,
raw = r#type,
present = "here",
many = ["a", b],
})
.unwrap();

// SpannedString accepts either a string literal or a bare identifier.
assert_eq!(lit.value(), "howdy");
assert_eq!(ident.value(), "word");
assert_eq!(keyword.value(), "mod");
assert_eq!(raw.value(), "r#type");
assert!(missing.is_none());
assert_eq!(present.unwrap().value(), "here");
assert_eq!(
many.iter().map(|s| s.value()).collect::<Vec<_>>(),
["a", "b"]
);

// The value can be parsed further.
assert_eq!(lit.parse::<syn::Ident>().unwrap(), "howdy");
assert_eq!(ident.parse::<syn::Ident>().unwrap(), "word");
assert_eq!(raw.parse::<syn::Ident>().unwrap(), "r#type");
assert_eq!(
keyword.parse::<syn::Ident>().unwrap_err().to_string(),
"expected identifier, found keyword `mod`"
);
assert_eq!(
lit.parse_with(syn::Path::parse_mod_style)
.unwrap()
.to_token_stream()
.to_string(),
"howdy"
);
assert_eq!(
SpannedString::new("\"", proc_macro2::Span::call_site())
.parse::<syn::Ident>()
.unwrap_err()
.to_string(),
"cannot parse string into token stream"
);

// Equality compares the value.
assert_eq!(
*lit,
SpannedString::new("howdy", proc_macro2::Span::call_site())
);
assert_ne!(lit, ident);
assert_eq!(lit.to_string(), "howdy");
}

#[test]
fn test_spanned_string_error() {
// Errors for the wrong kind of token match those for String, and a
// missing value is caught by deserialize_bytes.
for (tokens, expected) in [
(quote! { s = 123 }, "expected a string, but found `123`"),
(quote! { s = [a] }, "expected a string, but found `[a]`"),
(quote! { s = a b }, "expected `,` or nothing, but found `b`"),
(quote! { s = }, "expected a value following `=`"),
] {
#[derive(Deserialize)]
struct Test {
#[allow(dead_code)]
s: ParseWrapper<SpannedString>,
}
match from_tokenstream::<Test>(&tokens) {
Err(err) => assert_eq!(err.to_string(), expected),
Ok(_) => panic!("unexpected success for `{tokens}`"),
}
}
}

// Make sure ParseWrapper<syn::Type> is Hash
#[test]
fn test_parse_wrapper_hash() {
Expand Down
Loading
Loading