3个版本 (重大变更)

0.3.0 2024年8月1日
0.2.0 2023年7月19日
0.1.16 2023年3月24日

开发工具中排名第259

Download history 6595/week @ 2024-05-04 6261/week @ 2024-05-11 7394/week @ 2024-05-18 6687/week @ 2024-05-25 9389/week @ 2024-06-01 6555/week @ 2024-06-08 5326/week @ 2024-06-15 3746/week @ 2024-06-22 2658/week @ 2024-06-29 3078/week @ 2024-07-06 3715/week @ 2024-07-13 3719/week @ 2024-07-20 3318/week @ 2024-07-27 3356/week @ 2024-08-03 2555/week @ 2024-08-10 3028/week @ 2024-08-17

每月下载量12,767
用于m3rs_snippets

MIT/Apache

240KB
5.5K SLoC

prettyplease::unparse

github crates.io docs.rs build status

一个最小的syn语法树格式化打印器。


概述

这是一个将syn语法树转换为格式化良好的源代码字符串的格式化打印器。与rustfmt相比,这个库旨在适合任意生成的代码。

rustfmt优先考虑高质量输出,足以让你愿意花费职业生涯去审视它的输出——但这意味着一些重型算法,并且它有在难以格式化的代码上放弃的倾向(例如rustfmt#3697,并且还有更多类似的问题)。对于人类生成的代码来说,这并不是一个大问题,因为当代码高度嵌套时,人类自然会倾向于重构为更易于格式化的代码。但对于生成的代码来说,格式化器放弃的话,代码就完全无法阅读。

这个库使用了最简单的算法和数据结构,可以提供大约95%的rustfmt格式化输出的质量。在我的实际代码测试中,大约97-98%的输出行在rustfmt的格式化和这个crate的格式化之间是相同的。其余的行有略微不同的换行决定,但仍然清楚地遵循主导的现代Rust风格。

这个crate所做出的权衡非常适合那些你不会花职业生涯去审视的生成代码。例如,bindgen的输出,或者cargo-expand的输出。在这些情况下,更重要的是整个代码可以格式化而不会让格式化器放弃,而不是它是否完美。


功能矩阵

以下是该crate与rustc内建的AST格式化打印器和rustfmt的一些表面比较。下面的部分将更详细地比较每个库的输出。

prettyplease rustc rustfmt
对大或生成代码的非病态行为 💚
符合现代格式的语法(在本地不可区分于rustfmt) 💚 💚
吞吐量 60 MB/s 39 MB/s 2.8 MB/s
依赖项数量 3 72 66
包括依赖项的编译时间 2.4 秒 23.1 秒 29.8 秒
可以使用稳定的Rust编译器构建 💚
已发布到crates.io 💚
输出配置灵活 💚
旨在适应手动维护的源代码 💚

与rustfmt的比较

如果您不知道哪个输出文件是哪个,实际上几乎不可能区分开来——除非是rustfmt输出中的第435行,该行超过1000个字符长,因为rustfmt已经放弃了格式化该文件的部分

            match segments[5] {
                0 => write!(f, "::{}", ipv4),
                0xffff => write!(f, "::ffff:{}", ipv4),
                _ => unreachable!(),
            }
        } else { # [derive (Copy , Clone , Default)] struct Span { start : usize , len : usize , } let zeroes = { let mut longest = Span :: default () ; let mut current = Span :: default () ; for (i , & segment) in segments . iter () . enumerate () { if segment == 0 { if current . len == 0 { current . start = i ; } current . len += 1 ; if current . len > longest . len { longest = current ; } } else { current = Span :: default () ; } } longest } ; # [doc = " Write a colon-separated part of the address"] # [inline] fn fmt_subslice (f : & mut fmt :: Formatter < '_ > , chunk : & [u16]) -> fmt :: Result { if let Some ((first , tail)) = chunk . split_first () { write ! (f , "{:x}" , first) ? ; for segment in tail { f . write_char (':') ? ; write ! (f , "{:x}" , segment) ? ; } } Ok (()) } if zeroes . len > 1 { fmt_subslice (f , & segments [.. zeroes . start]) ? ; f . write_str ("::") ? ; fmt_subslice (f , & segments [zeroes . start + zeroes . len ..]) } else { fmt_subslice (f , & segments) } }
    } else {
        const IPV6_BUF_LEN: usize = (4 * 8) + 7;
        let mut buf = [0u8; IPV6_BUF_LEN];
        let mut buf_slice = &mut buf[..];

这是rustfmt在生成代码中崩溃的一个非常典型的表现——输入的一部分最终出现在一行上。另一种表现是,您正在编写一些代码,像负责任的开发者一样在保存时运行rustfmt,但过了一段时间后注意到它没有做什么。您引入一个故意的格式化问题,比如一个多余的缩进或分号,然后运行rustfmt来检查您的怀疑。不,它没有被清理掉——rustfmt只是没有格式化您正在工作的文件部分。

prettyplease库被设计成没有导致崩溃的病理案例;您给它的整个输入都将以一种“足够好”的形式进行格式化。

此外,rustfmt可能难以集成到项目中。它是用rustc的内部语法树编写的,因此不能由稳定的编译器构建。它的版本不是定期发布到crates.io的,所以在Cargo构建中,您需要将其作为git依赖项依赖,这阻止了您将crate发布到crates.io。您可以调用一个rustfmt二进制文件,但这将是每个开发者的系统上安装的rustfmt版本(如果有的话),这可能导致检查签入的由不同版本格式化的生成代码中出现虚假的差异。相比之下,prettyplease被设计成易于作为库拉入,并且编译速度快。


与rustc_ast_pretty的比较

这是当rustc打印源代码时使用的美化打印器,例如 rustc -Zunpretty=expanded。它也被标准库的 stringify! 在字符串化插值宏_rules AST片段时使用,以及由 dbg! 和生态系统中的许多宏间接使用。

Rustc的格式化大多数情况下是好的,但并不严格遵循当代Rust格式化的主导风格。有些事情永远不会写在一行上,比如这个 match 表达式,当然不会在闭合花括号前加逗号

fn eq(&self, other: &IpAddr) -> bool {
    match other { IpAddr::V4(v4) => self == v4, IpAddr::V6(_) => false, }
}

有些地方使用非4的倍数缩进,这肯定不是规范

pub const fn to_ipv6_mapped(&self) -> Ipv6Addr {
    let [a, b, c, d] = self.octets();
    Ipv6Addr{inner:
                 c::in6_addr{s6_addr:
                                 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF,
                                  0xFF, a, b, c, d],},}
}

尽管由于输入代码相当温和,链接中没有严重的例子,但总的来说,rustc_ast_pretty在生成代码上具有病理行为。它倾向于使用过多的水平缩进,并且迅速耗尽宽度

::std::io::_print(::core::fmt::Arguments::new_v1(&[""],
                                                 &match (&msg,) {
                                                      _args =>
                                                      [::core::fmt::ArgumentV1::new(_args.0,
                                                                                    ::core::fmt::Display::fmt)],
                                                  }));

上面的片段显然与现代rustfmt风格不同。相比之下,prettyplease被设计成输出与rustfmt格式化代码几乎无法区分。


示例

// [dependencies]
// prettyplease = "0.2"
// syn = { version = "2", default-features = false, features = ["full", "parsing"] }

const INPUT: &str = stringify! {
    use crate::{
          lazy::{Lazy, SyncLazy, SyncOnceCell}, panic,
        sync::{ atomic::{AtomicUsize, Ordering::SeqCst},
            mpsc::channel, Mutex, },
      thread,
    };
    impl<T, U> Into<U> for T where U: From<T> {
        fn into(self) -> U { U::from(self) }
    }
};

fn main() {
    let syntax_tree = syn::parse_file(INPUT).unwrap();
    let formatted = prettyplease::unparse(&syntax_tree);
    print!("{}", formatted);
}

算法说明

实现中使用的方法和术语来自 Derek C. Oppen, "Pretty Printing" (1979),rustc_ast_pretty也是基于该报告的,以及Graydon Hoare在2011年编写的rustc_ast_pretty的实现(自那时起由几十名志愿者维护者进行了现代化)。

本文描述了两种语言无关的交互式过程 Scan()Print()。特定语言的代码将输入数据结构分解成一系列 stringbreak 标记,以及用于分组的 beginend 标记。每个 beginend 范围可以识别为“一致中断”或“不一致中断”。如果一个组是一致中断的,那么如果整个内容无法放在一行上,每个 该组中的 break 标记都将收到一个换行符。例如,这对于 Rust 结构字面量或函数调用参数是合适的。如果一个组是不一致中断的,那么该组中的 string 标记将贪婪地放置在一行中,直到空间不足,并在那些无法放置下一 break 标记的位置进行换行。例如,这适用于 Rust 中大括号 use 语句的内容。

Scan 的任务是高效地积累有关组和中断的大小信息。对于每个 begin 标记,我们计算到匹配的 end 标记的距离,对于每个 break,我们计算到下一个 break 的距离。该算法使用一个环形缓冲区来存储大小尚未确定标记。环形缓冲区的最大大小受目标行长限制,并且不会无限增长,无论输入流中的嵌套有多深。这是因为一旦一个组足够大,精确的大小就不再影响换行决定,我们可以将其有效地视为“无限”。

Print 的任务是使用大小信息高效地为每个 begin 标记分配“中断”或“不中断”状态。此时,输出可以很容易地通过连接 string 标记并在中断组内的 break 标记处进行中断来构建。

利用这些原语(即巧妙地放置全有或全无的一致中断和不贪婪的不一致中断)以产生与 rustfmt 兼容的 Rust 语法树节点格式是一种有趣的挑战。

以下是某些 Rust 标记输入到美观打印算法中的可视化。一致中断的 beginend 对由 «» 表示,不一致中断由 表示,break· 表示,其余的非空白字符是 string

use crate::«{·
‹    lazy::«{·‹Lazy,· SyncLazy,· SyncOnceCell›·}»,·
    panic,·
    sync::«{·
‹        atomic::«{·‹AtomicUsize,· Ordering::SeqCst›·}»,·
        mpsc::channel,· Mutex›,·
    }»,·
    thread›,·
}»;·
«‹«impl<«·T‹›,· U‹›·»>» Into<«·U·»>· for T›·
where·
    U:‹ From<«·T·»>›,·
{·
«    fn into(·«·self·») -> U {·
‹        U::from(«·self·»)›·
»    }·
»}·

文中描述的算法对于产生本地与 rustfmt 风格不可区分的良好格式 Rust 代码尚不够充分。原因是,在论文中,假设完整的非空白内容与换行决定无关,Scan 和 Print 只控制空白(空格和换行)。在 rustfmt 的惯用格式中,情况并非如此。尾随逗号是一个例子;标点符号只在知道周围组的断裂与不断裂状态之后才能确定。

let _ = Struct { x: 0, y: true };

let _ = Struct {
    x: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
    y: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,   //<- trailing comma if the expression wrapped
};

《match》表达式的格式化是另一个问题;我们希望将小臂放在与模式相同的行上,将大臂放在花括号中。花括号标点符号(逗号和分号)的存在取决于臂是否适合一行。

match total_nanos.checked_add(entry.nanos as u64) {
    Some(n) => tmp = n,   //<- small arm, inline with comma
    None => {
        total_secs = total_secs
            .checked_add(total_nanos / NANOS_PER_SEC as u64)
            .expect("overflow in iter::sum over durations");
    }   //<- big arm, needs brace added, and also semicolon^
}

本库中的打印算法实现通过具有条件标点符号的令牌来适应所有这些情况,这些令牌的选择可以在知道组是否中断后延迟并填充。


许可证

根据您的选择,在Apache许可证,版本2.0MIT许可证下获得许可。
除非您明确说明,否则根据Apache-2.0许可证定义的,您有意提交并包含在本库中的任何贡献都应按上述方式双授权,不附加任何额外条款或条件。

依赖项

~250–680KB
~16K SLoC