Files
dioxus/packages/autofmt/src/prettier_please.rs
T
2024-04-08 22:15:52 -07:00

220 lines
7.3 KiB
Rust

use prettyplease::unparse;
use syn::{visit_mut::VisitMut, Expr, File, Item};
use crate::Writer;
impl Writer<'_> {
pub fn unparse_expr(&mut self, expr: &Expr) -> String {
struct ReplaceMacros<'a, 'b> {
writer: &'a mut Writer<'b>,
formatted_stack: Vec<String>,
}
impl VisitMut for ReplaceMacros<'_, '_> {
fn visit_stmt_mut(&mut self, _expr: &mut syn::Stmt) {
if let syn::Stmt::Macro(i) = _expr {
// replace the macro with a block that roughly matches the macro
if let Some("rsx" | "render") = i
.mac
.path
.segments
.last()
.map(|i| i.ident.to_string())
.as_deref()
{
// format the macro in place
// we'll use information about the macro to replace it with another formatted block
// once we've written out the unparsed expr from prettyplease, we can replace
// this dummy block with the actual formatted block
let formatted = crate::fmt_block_from_expr(
self.writer.raw_src,
syn::ExprMacro {
attrs: i.attrs.clone(),
mac: i.mac.clone(),
},
)
.unwrap();
*_expr = syn::Stmt::Expr(
syn::parse_quote!(dioxus_autofmt_block__________),
i.semi_token,
);
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted);
}
}
syn::visit_mut::visit_stmt_mut(self, _expr);
}
fn visit_expr_mut(&mut self, _expr: &mut syn::Expr) {
if let syn::Expr::Macro(i) = _expr {
// replace the macro with a block that roughly matches the macro
if let Some("rsx" | "render") = i
.mac
.path
.segments
.last()
.map(|i| i.ident.to_string())
.as_deref()
{
// format the macro in place
// we'll use information about the macro to replace it with another formatted block
// once we've written out the unparsed expr from prettyplease, we can replace
// this dummy block with the actual formatted block
let formatted = crate::fmt_block_from_expr(
self.writer.raw_src,
syn::ExprMacro {
attrs: i.attrs.clone(),
mac: i.mac.clone(),
},
)
.unwrap();
*_expr = syn::parse_quote!(dioxus_autofmt_block__________);
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted);
}
}
syn::visit_mut::visit_expr_mut(self, _expr);
}
}
// Visit the expr and replace the macros with formatted blocks
let mut replacer = ReplaceMacros {
writer: self,
formatted_stack: vec![],
};
// builds the expression stack
let mut modified_expr = expr.clone();
replacer.visit_expr_mut(&mut modified_expr);
// now unparsed with the modified expression
let mut unparsed = unparse_expr(&modified_expr);
// walk each line looking for the dioxus_autofmt_block__________ token
// if we find it, replace it with the formatted block
// if there's indentation we want to presreve it
// now we can replace the macros with the formatted blocks
for formatted in replacer.formatted_stack.drain(..) {
let fmted = if formatted.contains('\n') {
format!("rsx! {{{formatted}\n}}")
} else {
format!("rsx! {{{formatted}}}")
};
let mut out_fmt = String::new();
let mut whitespace = 0;
for line in unparsed.lines() {
if line.contains("dioxus_autofmt_block__________") {
whitespace = line.chars().take_while(|c| c.is_whitespace()).count();
break;
}
}
for (idx, fmt_line) in fmted.lines().enumerate() {
// Push the indentation
if idx > 0 {
out_fmt.push_str(&" ".repeat(whitespace));
}
out_fmt.push_str(fmt_line);
// Push a newline
out_fmt.push('\n');
}
// Remove the last newline
out_fmt.pop();
// Replace the dioxus_autofmt_block__________ token with the formatted block
unparsed = unparsed.replacen("dioxus_autofmt_block__________", &out_fmt, 1);
continue;
}
unparsed
}
}
/// Unparse an expression back into a string
///
/// This creates a new temporary file, parses the expression into it, and then formats the file.
/// This is a bit of a hack, but dtonlay doesn't want to support this very simple usecase, forcing us to clone the expr
pub fn unparse_expr(expr: &Expr) -> String {
let file = wrapped(expr);
let wrapped = unparse(&file);
unwrapped(wrapped)
}
// Split off the fn main and then cut the tabs off the front
fn unwrapped(raw: String) -> String {
let mut o = raw
.strip_prefix("fn main() {\n")
.unwrap()
.strip_suffix("}\n")
.unwrap()
.lines()
.map(|line| line.strip_prefix(" ").unwrap()) // todo: set this to tab level
.collect::<Vec<_>>()
.join("\n");
// remove the semicolon
o.pop();
o
}
fn wrapped(expr: &Expr) -> File {
File {
shebang: None,
attrs: vec![],
items: vec![
//
Item::Verbatim(quote::quote! {
fn main() {
#expr;
}
}),
],
}
}
#[test]
fn unparses_raw() {
let expr = syn::parse_str("1 + 1").unwrap();
let unparsed = unparse(&wrapped(&expr));
assert_eq!(unparsed, "fn main() {\n 1 + 1;\n}\n");
}
#[test]
fn unparses_completely() {
let expr = syn::parse_str("1 + 1").unwrap();
let unparsed = unparse_expr(&expr);
assert_eq!(unparsed, "1 + 1");
}
#[test]
fn unparses_let_guard() {
let expr = syn::parse_str("let Some(url) = &link.location").unwrap();
let unparsed = unparse_expr(&expr);
assert_eq!(unparsed, "let Some(url) = &link.location");
}
#[test]
fn weird_ifcase() {
let contents = r##"
fn main() {
move |_| timer.with_mut(|t| if t.started_at.is_none() { Some(Instant::now()) } else { None })
}
"##;
let expr: File = syn::parse_file(contents).unwrap();
let out = unparse(&expr);
println!("{}", out);
}