How to pretty print Syn AST?
Asked Answered
W

5

8

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.

Whatley answered 17/1, 2021 at 19:21 Comment(0)
R
5

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.

Richmond answered 17/1, 2021 at 21:6 Comment(0)
B
4

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())
);
Brave answered 25/5, 2022 at 18:5 Comment(0)
A
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);
}
Arjuna answered 8/11, 2022 at 11:39 Comment(1)
You shoudn't convert tokenstream to string, and then reparse it to a file ! You can just do let file = syn::parse2(ts).unwrap()Krute
L
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.
Luanneluanni answered 18/5, 2022 at 8:50 Comment(2)
4. It can format even longer lines of code that rustfmt struggles with, and are often generated by macros (see the repository for examples).Complicity
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.Greaten
F
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();
Fugitive answered 9/4, 2023 at 7:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.