Fix: #2604, Fix: #2240, Fix: #2341, Fix #1355 - Better error handling and and spaces handling in autofmt (#2736)

* add new autofmt sample
* Feat: implement rustfmt::skip support for rsx
* generally improve error handling with better expect messages
* wip: nested rsx formatting and expression formatting
* nested rsx formatting works
* collapse autofmt crate
* cast indent through macros
* use proper whitespace
* no more eating comments!
* Use proper error handling
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-07-30 18:36:13 -07:00
1 parent ef0202f999
commit 828cc502f1
47 files changed
+1621 -1055

No files matched your search

+313 -146
View File
@@ -1,142 +1,141 @@
use prettyplease::unparse;
use syn::{visit_mut::VisitMut, Expr, File, Item};
use dioxus_rsx::CallBody;
use syn::{parse::Parser, visit_mut::VisitMut, Expr, File, Item};
use crate::Writer;
use crate::{IndentOptions, 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>,
}
unparse_expr(expr, self.raw_src, &self.out.indent)
}
}
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();
const MARKER: &str = "dioxus_autofmt_block__________";
const MARKER_REPLACE: &str = "dioxus_autofmt_block__________! {}";
*_expr = syn::Stmt::Expr(
syn::parse_quote!(dioxus_autofmt_block__________),
i.semi_token,
);
pub fn unparse_expr(expr: &Expr, src: &str, cfg: &IndentOptions) -> String {
struct ReplaceMacros<'a> {
src: &'a str,
formatted_stack: Vec<String>,
cfg: &'a IndentOptions,
}
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted);
}
impl VisitMut for ReplaceMacros<'_> {
fn visit_macro_mut(&mut self, i: &mut syn::Macro) {
// replace the macro with a block that roughly matches the macro
if let Some("rsx" | "render") = i
.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 body = CallBody::parse_strict.parse2(i.tokens.clone()).unwrap();
let multiline = !Writer::is_short_rsx_call(&body.body.roots);
let mut formatted = {
let mut writer = Writer::new(self.src, self.cfg.clone());
_ = writer.write_body_nodes(&body.body.roots).ok();
writer.consume()
}
.unwrap();
// always push out the rsx to require a new line
i.path = syn::parse_str(MARKER).unwrap();
i.tokens = Default::default();
// Push out the indent level of the formatted block if it's multiline
if multiline || formatted.contains('\n') {
formatted = formatted
.lines()
.map(|line| format!("{}{line}", self.cfg.indent_str()))
.collect::<Vec<_>>()
.join("\n");
}
syn::visit_mut::visit_stmt_mut(self, _expr);
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted)
}
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();
syn::visit_mut::visit_macro_mut(self, i);
}
}
*_expr = syn::parse_quote!(dioxus_autofmt_block__________);
// Visit the expr and replace the macros with formatted blocks
let mut replacer = ReplaceMacros {
src,
cfg,
formatted_stack: vec![],
};
// Save this formatted block for later, when we apply it to the original expr
self.formatted_stack.push(formatted);
}
}
// builds the expression stack
let mut modified_expr = expr.clone();
replacer.visit_expr_mut(&mut modified_expr);
syn::visit_mut::visit_expr_mut(self, _expr);
// now unparsed with the modified expression
let mut unparsed = unparse_inner(&modified_expr);
// now we can replace the macros with the formatted blocks
for fmted in replacer.formatted_stack.drain(..) {
let is_multiline = fmted.contains('{');
let mut out_fmt = String::from("rsx! {");
if is_multiline {
out_fmt.push('\n');
} else {
out_fmt.push(' ');
}
let mut whitespace = 0;
for line in unparsed.lines() {
if line.contains(MARKER) {
whitespace = line.matches(cfg.indent_str()).count();
break;
}
}
// Visit the expr and replace the macros with formatted blocks
let mut replacer = ReplaceMacros {
writer: self,
formatted_stack: vec![],
};
let mut lines = fmted.lines().enumerate().peekable();
// 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;
}
while let Some((_idx, fmt_line)) = lines.next() {
// Push the indentation
if is_multiline {
out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
}
for (idx, fmt_line) in fmted.lines().enumerate() {
// Push the indentation
if idx > 0 {
out_fmt.push_str(&" ".repeat(whitespace));
}
// Calculate delta between indentations - the block indentation is too much
out_fmt.push_str(fmt_line);
out_fmt.push_str(fmt_line);
// Push a newline
// Push a newline if there's another line
if lines.peek().is_some() {
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;
}
if is_multiline {
out_fmt.push('\n');
out_fmt.push_str(&cfg.indent_str().repeat(whitespace));
} else {
out_fmt.push(' ');
}
// Replace the dioxus_autofmt_block__________ token with the formatted block
out_fmt.push('}');
unparsed = unparsed.replacen(MARKER_REPLACE, &out_fmt, 1);
continue;
}
// stylistic choice to trim whitespace around the expr
if unparsed.starts_with("{ ") && unparsed.ends_with(" }") {
let mut out_fmt = String::new();
out_fmt.push('{');
out_fmt.push_str(&unparsed[2..unparsed.len() - 2]);
out_fmt.push('}');
out_fmt
} else {
unparsed
}
}
@@ -145,9 +144,9 @@ impl Writer<'_> {
///
/// 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 {
pub fn unparse_inner(expr: &Expr) -> String {
let file = wrapped(expr);
let wrapped = unparse(&file);
let wrapped = prettyplease::unparse(&file);
unwrapped(wrapped)
}
@@ -164,7 +163,9 @@ fn unwrapped(raw: String) -> String {
.join("\n");
// remove the semicolon
o.pop();
if o.ends_with(';') {
o.pop();
}
o
}
@@ -184,36 +185,202 @@ fn wrapped(expr: &Expr) -> File {
}
}
#[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");
}
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::TokenStream;
#[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 })
fn fmt_block_from_expr(raw: &str, tokens: TokenStream, cfg: IndentOptions) -> Option<String> {
let body = CallBody::parse_strict.parse2(tokens).unwrap();
let mut writer = Writer::new(raw, cfg);
writer.write_body_nodes(&body.body.roots).ok()?;
writer.consume()
}
"##;
let expr: File = syn::parse_file(contents).unwrap();
let out = unparse(&expr);
println!("{}", out);
#[test]
fn unparses_raw() {
let expr = syn::parse_str("1 + 1").expect("Failed to parse");
let unparsed = prettyplease::unparse(&wrapped(&expr));
assert_eq!(unparsed, "fn main() {\n 1 + 1;\n}\n");
}
#[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 = prettyplease::unparse(&expr);
println!("{}", out);
}
#[test]
fn multiline_madness() {
let contents = r##"
{
{children.is_some().then(|| rsx! {
span {
class: "inline-block ml-auto hover:bg-gray-500",
onclick: move |evt| {
evt.cancel_bubble();
},
icons::icon_5 {}
{rsx! {
icons::icon_6 {}
}}
}
})}
{children.is_some().then(|| rsx! {
span {
class: "inline-block ml-auto hover:bg-gray-500",
onclick: move |evt| {
evt.cancel_bubble();
},
icons::icon_10 {}
}
})}
}
"##;
let expr: Expr = syn::parse_str(contents).unwrap();
let out = unparse_expr(&expr, contents, &IndentOptions::default());
println!("{}", out);
}
#[test]
fn write_body_no_indent() {
let src = r##"
span {
class: "inline-block ml-auto hover:bg-gray-500",
onclick: move |evt| {
evt.cancel_bubble();
},
icons::icon_10 {}
icons::icon_10 {}
icons::icon_10 {}
icons::icon_10 {}
div { "hi" }
div { div {} }
div { div {} div {} div {} }
{children}
{
some_big_long()
.some_big_long()
.some_big_long()
.some_big_long()
.some_big_long()
.some_big_long()
}
div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
Thing {
field: rsx! {
div { "hi" }
Component {
onrender: rsx! {
div { "hi" }
Component {
onclick: move |_| {
another_macro! {
div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
"gomg"
"hi!!"
"womh"
}
};
rsx! {
div { class: "max-w-lg lg:max-w-2xl mx-auto mb-16 text-center",
"gomg"
"hi!!"
"womh"
}
};
println!("hi")
},
onrender: move |_| {
let _ = 12;
let r = rsx! {
div { "hi" }
};
rsx! {
div { "hi" }
}
}
}
{
rsx! {
BarChart {
id: "bar-plot".to_string(),
x: value,
y: label
}
}
}
}
}
}
}
}
"##;
let tokens: TokenStream = syn::parse_str(src).unwrap();
let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
println!("{}", out);
}
#[test]
fn write_component_body() {
let src = r##"
div { class: "px-4", {is_current.then(|| rsx! { {children} })} }
"##;
let tokens: TokenStream = syn::parse_str(src).unwrap();
let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
println!("{}", out);
}
#[test]
fn weird_macro() {
let contents = r##"
fn main() {
move |_| {
drop_macro_semi! {
"something_very_long_something_very_long_something_very_long_something_very_long"
};
let _ = drop_macro_semi! {
"something_very_long_something_very_long_something_very_long_something_very_long"
};
drop_macro_semi! {
"something_very_long_something_very_long_something_very_long_something_very_long"
};
};
}
"##;
let expr: File = syn::parse_file(contents).unwrap();
let out = prettyplease::unparse(&expr);
println!("{}", out);
}
#[test]
fn comments_on_nodes() {
let src = r##"// hiasdasds
div {
attr: "value", // comment
div {}
"hi" // hello!
"hi" // hello!
"hi" // hello!
// hi!
}
"##;
let tokens: TokenStream = syn::parse_str(src).unwrap();
let out = fmt_block_from_expr(src, tokens, IndentOptions::default()).unwrap();
println!("{}", out);
}
}