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:
1 parent
ef0202f999
commit
828cc502f1
47 files changed
+1621
-1055
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user