7

I'm trying to use syn to create an AST from a Rust file and then using quote to write it to another. However, when I write it, it puts extra spaces between everything.

Note that the example below is just to demonstrate the minimum reproducible problem I'm having. I realize that if I just wanted to copy the code over I could copy the file but it doesn't fit my case and I need to use an AST.

pub fn build_file() {
    let current_dir = std::env::current_dir().expect("Unable to get current directory");
    let rust_file = std::fs::read_to_string(current_dir.join("src").join("lib.rs")).expect("Unable to read rust file");
    let ast = syn::parse_file(&rust_file).expect("Unable to create AST from rust file");

    match std::fs::write("src/utils.rs", quote::quote!(#ast).to_string());
}

The file that it creates an AST of is this:

#[macro_use]
extern crate foo;
mod test;
fn init(handle: foo::InitHandle) {
    handle.add_class::<Test::test>();
}

What it outputs is this:

# [macro_use] extern crate foo ; mod test ; fn init (handle : foo :: InitHandle) { handle . add_class :: < Test :: test > () ; }

I've even tried running it through rustfmt after writing it to the file like so:

utils::write_file("src/utils.rs", quote::quote!(#ast).to_string());

match std::process::Command::new("cargo").arg("fmt").output() {
    Ok(_v) => (),
    Err(e) => std::process::exit(1),
}

But it doesn't seem to make any difference.

Martin Geisler
  • 72,968
  • 25
  • 171
  • 229
Mr.Smithyyy
  • 2,157
  • 12
  • 49
  • 95

5 Answers5

6

The quote crate is not really concerned with pretty printing the generated code. You can run it through rustfmt, you just have to execute rustfmt src/utils.rs or cargo fmt -- src/utils.rs.

use std::fs;
use std::io;
use std::path::Path;
use std::process::Command;

fn write_and_fmt<P: AsRef<Path>, S: ToString>(path: P, code: S) -> io::Result<()> {
    fs::write(&path, code.to_string())?;

    Command::new("rustfmt")
        .arg(path.as_ref())
        .spawn()?
        .wait()?;

    Ok(())
}

Now you can just execute:

write_and_fmt("src/utils.rs", quote::quote!(#ast)).expect("unable to save or format");

See also "Any interest in a pretty-printing crate for Syn?" on the Rust forum.

vallentin
  • 23,478
  • 6
  • 59
  • 81
2

As Martin mentioned in his answer, prettyplease can be used to format code fragments, which can be quite useful when testing proc macro where the standard to_string() on proc_macro2::TokenStream is rather hard to read.

Here a code sample to pretty print a proc_macro2::TokenStream parsable as a syn::Item:

fn pretty_print_item(item: proc_macro2::TokenStream) -> String {
    let item = syn::parse2(item).unwrap();
    let file = syn::File {
        attrs: vec![],
        items: vec![item],
        shebang: None,
    };

    prettyplease::unparse(&file)
}

I used this in my tests to help me understand where is the wrong generated code:

assert_eq!(
    expected.to_string(),
    generate_event().to_string(),
    "\n\nActual:\n {}",
    pretty_print_item(generate_event())
);
Matt
  • 10,633
  • 3
  • 46
  • 49
1

Similar to other answers, I also use prettyplease.

I use this little trick to pretty-print a proc_macro2::TokenStream (e.g. what you get from calling quote::quote!):

fn pretty_print(ts: &proc_macro2::TokenStream) -> String {
    let file = syn::parse_file(&ts.to_string()).unwrap();
    prettyplease::unparse(&file)
}

Basically, I convert the token stream to an unformatted String, then parse that String into a syn::File, and then pass that to prettyplease package.

Usage:

#[test]
fn it_works() {
    let tokens = quote::quote! {
        struct Foo {
            bar: String,
            baz: u64,
        }
    };

    let formatted = pretty_print(&tokens);
    let expected = "struct Foo {\n    bar: String,\n    baz: u64,\n}\n";

    assert_eq!(formatted, expected);
}
Mohammad Banisaeid
  • 2,376
  • 27
  • 35
0

Please see the new prettyplease crate. Advantages:

  1. It can be used directly as a library.
  2. It can handle code fragments while rustfmt only handles full files.
  3. It is fast because it uses a simpler algorithm.
Martin Geisler
  • 72,968
  • 25
  • 171
  • 229
  • 4. It can format even longer lines of code that rustfmt struggles with, and are often generated by macros (see the repository for examples). – Chayim Friedman May 26 '22 at 00:15
  • 1
    How can it handle code fragments? As far as I can tell, any token stream needs to be fully parsed back into a `syn::File`. – detly Nov 01 '22 at 16:07
0

I use rust_format.

let configuration = rust_format::Config::new_str()
    .edition(rust_format::Edition::Rust2021)
    .option("blank_lines_lower_bound", "1");
let formatter = rust_format::RustFmt::from_config(configuration);

let content = formatter
    .format_tokens(tokens)
    .unwrap();
starball
  • 20,030
  • 7
  • 43
  • 238
Elmer
  • 9,147
  • 2
  • 48
  • 38