Skip to content

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

Open
@software-opal

Description

@software-opal

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) 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.

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

// 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::<Ident> 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:

let my_var_name = "let";
let ident = match syn::parse_str::<Ident>(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::<Ident>(&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 to convert the field name into a identifier:

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:

pub fn ident(ident: &str) -> Ident {
    match syn::parse_str::<Ident>(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: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
   1: core::fmt::write
   2: <std::io::IoSlice as core::fmt::Debug>::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: <rustc_expand::mbe::transcribe::Frame as core::iter::traits::iterator::Iterator>::next
  11: <rustc_expand::mbe::macro_rules::TokenSet as core::fmt::Debug>::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<proc_macro::bridge::client::BridgeStateL>::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<proc_macro::bridge::scoped_cell::ScopedCell<proc_macro::bridge::client::BridgeStateL>>::try_with
             at /rustc/8d69840ab92ea7f4d323420088dd8c9775f180cd\src\libstd\thread\local.rs:262
  18: std::thread::local::LocalKey<proc_macro::bridge::scoped_cell::ScopedCell<proc_macro::bridge::client::BridgeStateL>>::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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions