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,8 +1,5 @@
|
||||
//! The output buffer that supports some helpful methods
|
||||
//! These are separate from the input so we can lend references between the two
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
|
||||
use std::fmt::{Result, Write};
|
||||
|
||||
|
||||
@@ -3,37 +3,66 @@
|
||||
//! Returns all macros that match a pattern. You can use this information to autoformat them later
|
||||
|
||||
use proc_macro2::LineColumn;
|
||||
use syn::{visit::Visit, File, Macro};
|
||||
use syn::{visit::Visit, File, Macro, Meta};
|
||||
|
||||
type CollectedMacro<'a> = &'a Macro;
|
||||
|
||||
pub fn collect_from_file(file: &File) -> Vec<CollectedMacro<'_>> {
|
||||
let mut macros = vec![];
|
||||
MacroCollector::visit_file(
|
||||
&mut MacroCollector {
|
||||
macros: &mut macros,
|
||||
},
|
||||
file,
|
||||
);
|
||||
let mut collector = MacroCollector::new(&mut macros);
|
||||
MacroCollector::visit_file(&mut collector, file);
|
||||
macros
|
||||
}
|
||||
|
||||
struct MacroCollector<'a, 'b> {
|
||||
macros: &'a mut Vec<CollectedMacro<'b>>,
|
||||
skip_count: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'b> MacroCollector<'a, 'b> {
|
||||
fn new(macros: &'a mut Vec<CollectedMacro<'b>>) -> Self {
|
||||
Self {
|
||||
macros,
|
||||
skip_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visit<'b> for MacroCollector<'a, 'b> {
|
||||
fn visit_macro(&mut self, i: &'b Macro) {
|
||||
if let Some("rsx" | "render") = i
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map(|i| i.ident.to_string())
|
||||
.as_deref()
|
||||
{
|
||||
self.macros.push(i)
|
||||
// Visit the regular stuff - this will also ensure paths/attributes are visited
|
||||
syn::visit::visit_macro(self, i);
|
||||
|
||||
let name = &i.path.segments.last().map(|i| i.ident.to_string());
|
||||
if let Some("rsx" | "render") = name.as_deref() {
|
||||
if self.skip_count == 0 {
|
||||
self.macros.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attributes can occur on stmts and items - we need to make sure the stack is reset when we exit
|
||||
// this means we save the skipped length and set it back to its original length
|
||||
fn visit_stmt(&mut self, i: &'b syn::Stmt) {
|
||||
let skipped_len = self.skip_count;
|
||||
syn::visit::visit_stmt(self, i);
|
||||
self.skip_count = skipped_len;
|
||||
}
|
||||
|
||||
fn visit_item(&mut self, i: &'b syn::Item) {
|
||||
let skipped_len = self.skip_count;
|
||||
syn::visit::visit_item(self, i);
|
||||
self.skip_count = skipped_len;
|
||||
}
|
||||
|
||||
fn visit_attribute(&mut self, i: &'b syn::Attribute) {
|
||||
// we need to communicate that this stmt is skipped up the tree
|
||||
if attr_is_rustfmt_skip(i) {
|
||||
self.skip_count += 1;
|
||||
}
|
||||
|
||||
syn::visit::visit_attribute(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn byte_offset(input: &str, location: LineColumn) -> usize {
|
||||
@@ -49,10 +78,32 @@ pub fn byte_offset(input: &str, location: LineColumn) -> usize {
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
/// Check if an attribute is a rustfmt skip attribute
|
||||
fn attr_is_rustfmt_skip(i: &syn::Attribute) -> bool {
|
||||
match &i.meta {
|
||||
Meta::Path(path) => {
|
||||
path.segments.len() == 2
|
||||
&& matches!(i.style, syn::AttrStyle::Outer)
|
||||
&& path.segments[0].ident == "rustfmt"
|
||||
&& path.segments[1].ident == "skip"
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_file_and_collects_rsx_macros() {
|
||||
let contents = include_str!("../tests/samples/long.rsx");
|
||||
let parsed = syn::parse_file(contents).unwrap();
|
||||
let parsed = syn::parse_file(contents).expect("parse file okay");
|
||||
let macros = collect_from_file(&parsed);
|
||||
assert_eq!(macros.len(), 3);
|
||||
}
|
||||
|
||||
/// Ensure that we only collect non-skipped macros
|
||||
#[test]
|
||||
fn dont_collect_skipped_macros() {
|
||||
let contents = include_str!("../tests/samples/skip.rsx");
|
||||
let parsed = syn::parse_file(contents).expect("parse file okay");
|
||||
let macros = collect_from_file(&parsed);
|
||||
assert_eq!(macros.len(), 2);
|
||||
}
|
||||
+35
-32
@@ -4,14 +4,13 @@
|
||||
|
||||
use crate::writer::*;
|
||||
use dioxus_rsx::{BodyNode, CallBody};
|
||||
use proc_macro2::LineColumn;
|
||||
use syn::{parse::Parser, ExprMacro};
|
||||
use proc_macro2::{LineColumn, Span};
|
||||
use syn::parse::Parser;
|
||||
|
||||
mod buffer;
|
||||
mod collect_macros;
|
||||
mod indent;
|
||||
mod prettier_please;
|
||||
mod rsx_block;
|
||||
mod writer;
|
||||
|
||||
pub use indent::{IndentOptions, IndentType};
|
||||
@@ -37,6 +36,16 @@ pub struct FormattedBlock {
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
|
||||
///
|
||||
/// It accepts
|
||||
#[deprecated(note = "Use try_fmt_file instead - this function panics on error.")]
|
||||
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
let parsed =
|
||||
syn::parse_file(contents).expect("fmt_file should only be called on valid syn::File files");
|
||||
try_fmt_file(contents, &parsed, indent).expect("Failed to format file")
|
||||
}
|
||||
|
||||
/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
|
||||
///
|
||||
/// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead.
|
||||
@@ -45,19 +54,27 @@ pub struct FormattedBlock {
|
||||
/// back to the file precisely.
|
||||
///
|
||||
/// Nested blocks of RSX will be handled automatically
|
||||
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
///
|
||||
/// This returns an error if the rsx itself is invalid.
|
||||
///
|
||||
/// Will early return if any of the expressions are not complete. Even though we *could* return the
|
||||
/// expressions, eventually we'll want to pass off expression formatting to rustfmt which will reject
|
||||
/// those.
|
||||
pub fn try_fmt_file(
|
||||
contents: &str,
|
||||
parsed: &syn::File,
|
||||
indent: IndentOptions,
|
||||
) -> syn::Result<Vec<FormattedBlock>> {
|
||||
let mut formatted_blocks = Vec::new();
|
||||
|
||||
let parsed = syn::parse_file(contents).unwrap();
|
||||
let macros = collect_macros::collect_from_file(&parsed);
|
||||
let macros = collect_macros::collect_from_file(parsed);
|
||||
|
||||
// No macros, no work to do
|
||||
if macros.is_empty() {
|
||||
return formatted_blocks;
|
||||
return Ok(formatted_blocks);
|
||||
}
|
||||
|
||||
let mut writer = Writer::new(contents);
|
||||
writer.out.indent = indent;
|
||||
let mut writer = Writer::new(contents, indent);
|
||||
|
||||
// Don't parse nested macros
|
||||
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||
@@ -69,14 +86,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let body = match item.parse_body_with(CallBody::parse_strict) {
|
||||
Ok(v) => v,
|
||||
//there is aparsing error, we give up and don't format the rsx
|
||||
Err(e) => {
|
||||
eprintln!("Error while parsing rsx {:?} ", e);
|
||||
return formatted_blocks;
|
||||
}
|
||||
};
|
||||
let body = item.parse_body_with(CallBody::parse_strict)?;
|
||||
|
||||
let rsx_start = macro_path.span().start();
|
||||
|
||||
@@ -86,15 +96,16 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
.count_indents(writer.src[rsx_start.line - 1]);
|
||||
|
||||
// TESTME
|
||||
// If we fail to parse this macro then we have no choice to give up and return what we've got
|
||||
// Writing *should* not fail but it's possible that it does
|
||||
if writer.write_rsx_call(&body.body).is_err() {
|
||||
return formatted_blocks;
|
||||
let span = writer.invalid_exprs.pop().unwrap_or_else(Span::call_site);
|
||||
return Err(syn::Error::new(span, "Failed emit valid rsx - likely due to partially complete expressions in the rsx! macro"));
|
||||
}
|
||||
|
||||
// writing idents leaves the final line ended at the end of the last ident
|
||||
if writer.out.buf.contains('\n') {
|
||||
writer.out.new_line().unwrap();
|
||||
writer.out.tab().unwrap();
|
||||
_ = writer.out.new_line();
|
||||
_ = writer.out.tab();
|
||||
}
|
||||
|
||||
let span = item.delimiter.span().join();
|
||||
@@ -124,7 +135,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
});
|
||||
}
|
||||
|
||||
formatted_blocks
|
||||
Ok(formatted_blocks)
|
||||
}
|
||||
|
||||
/// Write a Callbody (the rsx block) to a string
|
||||
@@ -132,14 +143,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
/// If the tokens can't be formatted, this returns None. This is usually due to an incomplete expression
|
||||
/// that passed partial expansion but failed to parse.
|
||||
pub fn write_block_out(body: &CallBody) -> Option<String> {
|
||||
let mut buf = Writer::new("");
|
||||
buf.write_rsx_call(&body.body).ok()?;
|
||||
buf.consume()
|
||||
}
|
||||
|
||||
pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
||||
let body = CallBody::parse_strict.parse2(expr.mac.tokens).unwrap();
|
||||
let mut buf = Writer::new(raw);
|
||||
let mut buf = Writer::new("", IndentOptions::default());
|
||||
buf.write_rsx_call(&body.body).ok()?;
|
||||
buf.consume()
|
||||
}
|
||||
@@ -147,8 +151,7 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
||||
pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
|
||||
let body = CallBody::parse_strict.parse_str(block).unwrap();
|
||||
|
||||
let mut buf = Writer::new(block);
|
||||
buf.out.indent = indent;
|
||||
let mut buf = Writer::new(block, indent);
|
||||
buf.out.indent_level = indent_level;
|
||||
buf.write_rsx_call(&body.body).ok()?;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,444 +0,0 @@
|
||||
use crate::{prettier_please::unparse_expr, Writer};
|
||||
use dioxus_rsx::*;
|
||||
use proc_macro2::Span;
|
||||
use quote::ToTokens;
|
||||
use std::{
|
||||
fmt::Result,
|
||||
fmt::{self, Write},
|
||||
};
|
||||
use syn::{spanned::Spanned, token::Brace, Expr};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ShortOptimization {
|
||||
/// Special because we want to print the closing bracket immediately
|
||||
///
|
||||
/// IE
|
||||
/// `div {}` instead of `div { }`
|
||||
Empty,
|
||||
|
||||
/// Special optimization to put everything on the same line and add some buffer spaces
|
||||
///
|
||||
/// IE
|
||||
///
|
||||
/// `div { "asdasd" }` instead of a multiline variant
|
||||
Oneliner,
|
||||
|
||||
/// Optimization where children flow but props remain fixed on top
|
||||
PropsOnTop,
|
||||
|
||||
/// The noisiest optimization where everything flows
|
||||
NoOpt,
|
||||
}
|
||||
|
||||
impl Writer<'_> {
|
||||
/// Basically elements and components are the same thing
|
||||
///
|
||||
/// This writes the contents out for both in one function, centralizing the annoying logic like
|
||||
/// key handling, breaks, closures, etc
|
||||
pub fn write_rsx_block(
|
||||
&mut self,
|
||||
attributes: &[Attribute],
|
||||
spreads: &[Spread],
|
||||
children: &[BodyNode],
|
||||
brace: &Brace,
|
||||
) -> Result {
|
||||
// decide if we have any special optimizations
|
||||
// Default with none, opt the cases in one-by-one
|
||||
let mut opt_level = ShortOptimization::NoOpt;
|
||||
|
||||
// check if we have a lot of attributes
|
||||
let attr_len = self.is_short_attrs(attributes, spreads);
|
||||
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
|
||||
let children_len = self.is_short_children(children);
|
||||
let is_small_children = children_len.is_some();
|
||||
|
||||
// if we have one long attribute and a lot of children, place the attrs on top
|
||||
if is_short_attr_list && !is_small_children {
|
||||
opt_level = ShortOptimization::PropsOnTop;
|
||||
}
|
||||
|
||||
// even if the attr is long, it should be put on one line
|
||||
// However if we have childrne we need to just spread them out for readability
|
||||
if !is_short_attr_list && attributes.len() <= 1 && spreads.is_empty() {
|
||||
if children.is_empty() {
|
||||
opt_level = ShortOptimization::Oneliner;
|
||||
} else {
|
||||
opt_level = ShortOptimization::PropsOnTop;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have few children and few attributes, make it a one-liner
|
||||
if is_short_attr_list && is_small_children {
|
||||
if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
|
||||
opt_level = ShortOptimization::Oneliner;
|
||||
} else {
|
||||
opt_level = ShortOptimization::PropsOnTop;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's nothing at all, empty optimization
|
||||
if attributes.is_empty() && children.is_empty() && spreads.is_empty() {
|
||||
opt_level = ShortOptimization::Empty;
|
||||
|
||||
// Write comments if they exist
|
||||
self.write_todo_body(brace)?;
|
||||
}
|
||||
|
||||
// multiline handlers bump everything down
|
||||
if attr_len > 1000 || self.out.indent.split_line_attributes() {
|
||||
opt_level = ShortOptimization::NoOpt;
|
||||
}
|
||||
|
||||
let has_children = !children.is_empty();
|
||||
|
||||
match opt_level {
|
||||
ShortOptimization::Empty => {}
|
||||
ShortOptimization::Oneliner => {
|
||||
write!(self.out, " ")?;
|
||||
|
||||
self.write_attributes(attributes, spreads, true, brace, has_children)?;
|
||||
|
||||
if !children.is_empty() && !attributes.is_empty() {
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
|
||||
for child in children.iter() {
|
||||
self.write_ident(child)?;
|
||||
}
|
||||
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
|
||||
ShortOptimization::PropsOnTop => {
|
||||
if !attributes.is_empty() {
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
|
||||
self.write_attributes(attributes, spreads, true, brace, has_children)?;
|
||||
|
||||
if !children.is_empty() {
|
||||
self.write_body_indented(children)?;
|
||||
}
|
||||
|
||||
self.out.tabbed_line()?;
|
||||
}
|
||||
|
||||
ShortOptimization::NoOpt => {
|
||||
self.write_attributes(attributes, spreads, false, brace, has_children)?;
|
||||
|
||||
if !children.is_empty() {
|
||||
self.write_body_indented(children)?;
|
||||
}
|
||||
|
||||
self.out.tabbed_line()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_attributes(
|
||||
&mut self,
|
||||
attributes: &[Attribute],
|
||||
spreads: &[Spread],
|
||||
props_same_line: bool,
|
||||
brace: &Brace,
|
||||
has_children: bool,
|
||||
) -> Result {
|
||||
enum AttrType<'a> {
|
||||
Attr(&'a Attribute),
|
||||
Spread(&'a Spread),
|
||||
}
|
||||
|
||||
let mut attr_iter = attributes
|
||||
.iter()
|
||||
.map(AttrType::Attr)
|
||||
.chain(spreads.iter().map(AttrType::Spread))
|
||||
.peekable();
|
||||
|
||||
while let Some(attr) = attr_iter.next() {
|
||||
self.out.indent_level += 1;
|
||||
|
||||
if !props_same_line {
|
||||
self.write_attr_comments(
|
||||
brace,
|
||||
match attr {
|
||||
AttrType::Attr(attr) => attr.span(),
|
||||
AttrType::Spread(attr) => attr.expr.span(),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
self.out.indent_level -= 1;
|
||||
|
||||
if !props_same_line {
|
||||
self.out.indented_tabbed_line()?;
|
||||
}
|
||||
|
||||
match attr {
|
||||
AttrType::Attr(attr) => self.write_attribute(attr)?,
|
||||
AttrType::Spread(attr) => self.write_spread_attribute(&attr.expr)?,
|
||||
}
|
||||
|
||||
if attr_iter.peek().is_some() {
|
||||
write!(self.out, ",")?;
|
||||
|
||||
if props_same_line {
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_attributes = !attributes.is_empty() || !spreads.is_empty();
|
||||
|
||||
if has_attributes && has_children {
|
||||
write!(self.out, ",")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_attribute(&mut self, attr: &Attribute) -> Result {
|
||||
self.write_attribute_name(&attr.name)?;
|
||||
|
||||
// if the attribute is a shorthand, we don't need to write the colon, just the name
|
||||
if !attr.can_be_shorthand() {
|
||||
write!(self.out, ": ")?;
|
||||
self.write_attribute_value(&attr.value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_attribute_name(&mut self, attr: &AttributeName) -> Result {
|
||||
match attr {
|
||||
AttributeName::BuiltIn(name) => {
|
||||
write!(self.out, "{}", name)?;
|
||||
}
|
||||
AttributeName::Custom(name) => {
|
||||
write!(self.out, "{}", name.to_token_stream())?;
|
||||
}
|
||||
AttributeName::Spread(_) => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_attribute_value(&mut self, value: &AttributeValue) -> Result {
|
||||
match value {
|
||||
AttributeValue::IfExpr(if_chain) => {
|
||||
self.write_attribute_if_chain(if_chain)?;
|
||||
}
|
||||
AttributeValue::AttrLiteral(value) => {
|
||||
write!(self.out, "{value}")?;
|
||||
}
|
||||
AttributeValue::Shorthand(value) => {
|
||||
write!(self.out, "{value}")?;
|
||||
}
|
||||
AttributeValue::EventTokens(closure) => {
|
||||
self.write_partial_closure(closure)?;
|
||||
}
|
||||
|
||||
AttributeValue::AttrExpr(value) => {
|
||||
let Ok(expr) = value.as_expr() else {
|
||||
return Err(fmt::Error);
|
||||
};
|
||||
|
||||
let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
|
||||
self.write_mulitiline_tokens(pretty_expr)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_attribute_if_chain(&mut self, if_chain: &IfAttributeValue) -> Result {
|
||||
write!(self.out, "if {} {{ ", unparse_expr(&if_chain.condition))?;
|
||||
self.write_attribute_value(&if_chain.then_value)?;
|
||||
write!(self.out, " }}")?;
|
||||
match if_chain.else_value.as_deref() {
|
||||
Some(AttributeValue::IfExpr(else_if_chain)) => {
|
||||
write!(self.out, "else ")?;
|
||||
self.write_attribute_if_chain(else_if_chain)?;
|
||||
}
|
||||
Some(other) => {
|
||||
write!(self.out, "else {{")?;
|
||||
self.write_attribute_value(other)?;
|
||||
write!(self.out, " }}")?;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_mulitiline_tokens(&mut self, out: String) -> Result {
|
||||
let mut lines = out.split('\n').peekable();
|
||||
let first = lines.next().unwrap();
|
||||
|
||||
// a one-liner for whatever reason
|
||||
// Does not need a new line
|
||||
if lines.peek().is_none() {
|
||||
write!(self.out, "{first}")?;
|
||||
} else {
|
||||
writeln!(self.out, "{first}")?;
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
self.out.indented_tab()?;
|
||||
write!(self.out, "{line}")?;
|
||||
if lines.peek().is_none() {
|
||||
write!(self.out, "")?;
|
||||
} else {
|
||||
writeln!(self.out)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write out the special PartialClosure type from the rsx crate
|
||||
/// Basically just write token by token until we hit the block and then try and format *that*
|
||||
/// We can't just ToTokens
|
||||
fn write_partial_closure(&mut self, closure: &PartialClosure) -> Result {
|
||||
// Write the pretty version of the closure
|
||||
if let Ok(expr) = closure.as_expr() {
|
||||
let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
|
||||
self.write_mulitiline_tokens(pretty_expr)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If we can't parse the closure, writing it is also a failure
|
||||
// rustfmt won't be able to parse it either so no point in trying
|
||||
Err(fmt::Error)
|
||||
}
|
||||
|
||||
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
|
||||
let formatted = unparse_expr(attr);
|
||||
|
||||
let mut lines = formatted.lines();
|
||||
|
||||
let first_line = lines.next().unwrap();
|
||||
|
||||
write!(self.out, "..{first_line}")?;
|
||||
for line in lines {
|
||||
self.out.indented_tabbed_line()?;
|
||||
write!(self.out, "{line}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// make sure the comments are actually relevant to this element.
|
||||
// test by making sure this element is the primary element on this line
|
||||
pub fn current_span_is_primary(&self, location: Span) -> bool {
|
||||
let start = location.start();
|
||||
let line_start = start.line - 1;
|
||||
|
||||
let beginning = self
|
||||
.src
|
||||
.get(line_start)
|
||||
.filter(|this_line| this_line.len() > start.column)
|
||||
.map(|this_line| this_line[..start.column].trim())
|
||||
.unwrap_or_default();
|
||||
|
||||
beginning.is_empty()
|
||||
}
|
||||
|
||||
// check if the children are short enough to be on the same line
|
||||
// We don't have the notion of current line depth - each line tries to be < 80 total
|
||||
// returns the total line length if it's short
|
||||
// returns none if the length exceeds the limit
|
||||
// I think this eventually becomes quadratic :(
|
||||
pub fn is_short_children(&mut self, children: &[BodyNode]) -> Option<usize> {
|
||||
if children.is_empty() {
|
||||
// todo: allow elements with comments but no children
|
||||
// like div { /* comment */ }
|
||||
// or
|
||||
// div {
|
||||
// // some helpful
|
||||
// }
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
// Any comments push us over the limit automatically
|
||||
if self.children_have_comments(children) {
|
||||
return None;
|
||||
}
|
||||
|
||||
match children {
|
||||
[BodyNode::Text(ref text)] => Some(text.input.to_string_with_quotes().len()),
|
||||
|
||||
// TODO: let rawexprs to be inlined
|
||||
[BodyNode::RawExpr(ref expr)] => Some(get_expr_length(expr.span())),
|
||||
|
||||
// TODO: let rawexprs to be inlined
|
||||
[BodyNode::Component(ref comp)] if comp.fields.is_empty() => Some(
|
||||
comp.name
|
||||
.segments
|
||||
.iter()
|
||||
.map(|s| s.ident.to_string().len() + 2)
|
||||
.sum::<usize>(),
|
||||
),
|
||||
|
||||
// Feedback on discord indicates folks don't like combining multiple children on the same line
|
||||
// We used to do a lot of math to figure out if we should expand out the line, but folks just
|
||||
// don't like it.
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn children_have_comments(&self, children: &[BodyNode]) -> bool {
|
||||
for child in children {
|
||||
if self.current_span_is_primary(child.span()) {
|
||||
'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
|
||||
match (line.trim().starts_with("//"), line.is_empty()) {
|
||||
(true, _) => return true,
|
||||
(_, true) => continue 'line,
|
||||
_ => break 'line,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// empty everything except for some comments
|
||||
fn write_todo_body(&mut self, brace: &Brace) -> fmt::Result {
|
||||
let span = brace.span.span();
|
||||
let start = span.start();
|
||||
let end = span.end();
|
||||
|
||||
if start.line == end.line {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
writeln!(self.out)?;
|
||||
|
||||
for idx in start.line..end.line {
|
||||
let line = &self.src[idx];
|
||||
if line.trim().starts_with("//") {
|
||||
for _ in 0..self.out.indent_level + 1 {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
writeln!(self.out, "{}", line.trim()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..self.out.indent_level {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_expr_length(span: Span) -> usize {
|
||||
let (start, end) = (span.start(), span.end());
|
||||
if start.line == end.line {
|
||||
end.column - start.column
|
||||
} else {
|
||||
10000
|
||||
}
|
||||
}
|
||||
+766
-270
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user