dtolnay / dtolnay/proc-macro2

Ident::new documentation is not clear that using a `r#` identifier will cause a panic

Open
#234 0 comments 0 reactions 0 assignees View on GitHub
docs
Dominant language
Rust
Stars
934
Forks
135
PR merge metrics
No merged PRs in 30d

Description

It would be good to have some extra information in the documentation about how to handle `r#`-prefixed identifiers. For example:

> Special care should be taken when using Rust reserved words(like `let`, `match`, `type`, [more here](https://doc.rust-lang.org/reference/keywords.html)) in an Ident. The `Ident::new` will **not** error when called with an keyword, but Rust will fail to compile any generated code that uses it.
> ```rust
> let special_ident = Ident::new("let", Span::call_site());
> // Will run fine, but if this is used in generated code, the generated code will fail to compile
> ```
> Instead, you can use the `Ident::new_raw`(currently semver exempt) to generate a Ident that will generate valid Rust code when used as an identifier
> ```rust
> // Is this call correct? Do I need to call this with "r#let" instead.
> let special_ident = Ident::new_raw("let", Span::call_site());
> // `special_ident` is now able to be used in code generation as an identifier
> // it will appear prefixed by `r#` to escape the keyword.
> ```
> You may be tempted to use the `Ident::new` with a `"r#..."` argument, however this will cause the compiler to crash during ?code generation?.
> An alternative to using the `Ident::new_raw` function(and opting-in to semver exempt functionality) is to use the `syn::parse_str::` function. This function also correctly handles `r#` identifiers. This can be combined to create a function that will generate the correct `Ident` instance for both valid identifiers and keywords:
>
> ```rust
> let my_var_name = "let";
> let ident = match syn::parse_str::(ident) {
> Ok(ident) => ident, // A valid identifier
> Err(_) => // Not a valid identifier, could be a keyword.
> // Call `.unwrap` to panic on a never-valid identifier like "" or "123"
> syn::parse_str::(&format!("r#{}", ident)).unwrap(),
> };
> ```

-------
## Some background about how I ended up here

I'm trying to generate some Rust structs from a file; and the names could be rust identifiers. In my code I used the [`Ident::new`](https://docs.rs/proc-macro2/1.0.17/proc_macro2/struct.Ident.html#method.new) to convert the field name into a identifier:
```rust
let ident = Ident::new(field_name, Span::call_site())
...
```

This generates a valid ident, but the macro that uses it won't compile when the `field_name` is a rust keyword(like `let`). My first attempt to fix this involved this:

```rust
pub fn ident(ident: &str) -> Ident {
match syn::parse_str::(ident) {
Ok(mut ident) => {
ident.set_span(Span::call_site());
ident
}
Err(_) => Ident::new(format!("r#{}", ident), Span::call_site()),
}
}
```

However using this code causes the compiler to crash during the build.

```
thread 'rustc' panicked at '`"r#type"` is not a valid identifier', src\librustc_expand\proc_macro_server.rs:329:13
stack backtrace:
0: ::fmt
1: core::fmt::write
2: ::fmt
3: std::panicking::take_hook
4: std::panicking::take_hook
5: rustc_driver::report_ice
6: std::panicking::rust_panic_with_hook
7: rust_begin_unwind
8: std::panicking::begin_panic_fmt
9: rustc_expand::expand::AstFragment::make_variants
10: ::next
11: ::fmt
12: _rust_maybe_catch_panic
13: rustc_expand::base::MacEager::ty
14: rustc_expand::base::MacEager::ty
15: proc_macro::bridge::scoped_cell::ScopedCell::replace
at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\/src\libproc_macro\bridge\scoped_cell.rs:74
16: proc_macro::bridge::client::{{impl}}::with::{{closure}}
at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\/src\libproc_macro\bridge\client.rs:284
17: std::thread::local::LocalKey>::try_with
at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\src\libstd\thread\local.rs:262
18: std::thread::local::LocalKey>::with
at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\src\libstd\thread\local.rs:239
19: proc_macro::bridge::client::BridgeState::with
at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\/src\libproc_macro\bridge\client.rs:283
20: proc_macro::bridge::Bridge::with
at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\/src\libproc_macro\bridge\client.rs:314
21: proc_macro::bridge::client::Ident::new
at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\/src\libproc_macro\bridge\client.rs:230
22: proc_macro2::imp::Ident::new
at C:\Users\leesy\.cargo\registry\src\github.com-1ecc6299db9ec823\proc-macro2-1.0.12\src\wrapper.rs:631
23: proc_macro2::Ident::new
at C:\Users\leesy\.cargo\registry\src\github.com-1ecc6299db9ec823\proc-macro2-1.0.12\src\lib.rs:866
```

The docs don't make it clear that this setup of calling `Ident::new` with a `r#`-prefixed ident won't generate a valid Ident. Without the `Ident::new_raw` being stableized the only way I can see(from my very rudimentary understanding on how this magic works) is to call `syn::parse_str` twice:
```rust
pub fn ident(ident: &str) -> Ident {
let mut ident = match syn::parse_str::(ident) {
Ok(ident) => ident,
Err(_) => syn::parse_str::(&format!("r#{}", ident)).unwrap(),
};
ident.set_span(Span::call_site());
ident
}
```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.