47 个版本

0.2.20 2024 年 5 月 7 日
0.2.17 2024 年 3 月 25 日
0.2.15 2023 年 9 月 7 日
0.2.12 2023 年 7 月 21 日
0.1.9 2022 年 3 月 27 日

#3值格式化

Download history 1043067/week @ 2024-05-02 1102802/week @ 2024-05-09 1173322/week @ 2024-05-16 1120159/week @ 2024-05-23 1259234/week @ 2024-05-30 1245179/week @ 2024-06-06 1225564/week @ 2024-06-13 1218024/week @ 2024-06-20 1164916/week @ 2024-06-27 1038120/week @ 2024-07-04 1128477/week @ 2024-07-11 1158386/week @ 2024-07-18 1186763/week @ 2024-07-25 1184720/week @ 2024-08-01 1273280/week @ 2024-08-08 1262286/week @ 2024-08-15

5,128,156 每月下载量
3,795 个 crates (237 直接使用) 中使用

MIT/Apache

240KB
5.5K SLoC

prettyplease::unparse

github crates.io docs.rs build status

一个最简单的 syn 语法树美化打印器。


概述

这是一个将 syn 语法树转换为格式良好的源代码 String 的美化打印器。与 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行,因为rustfmt已经放弃格式化文件的这一部分,导致该行超过1000个字符。

            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!在字符串化一个插值的macro_rules AST片段(如$:expr)时使用,以及通过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)》(http://i.stanford.edu/pub/cstr/reports/cs/tr/79/770/CS-TR-79-770.pdf),该文档也是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标记分配“断行”或“不断行”的状态。此时,输出可以通过连接字符串标记并在断行组中的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惯用格式化的Rust中,情况并非如此。尾随逗号就是一个例子;标点符号只有在知道周围组的断裂与非断裂状态后才知道。

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

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

匹配表达式的格式化是另一个例子;我们希望小分支与模式在同一行上,而大分支则用花括号包裹。花括号标点符号、逗号和分号的存在都取决于分支是否适合在同一行上。

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 License,Version 2.0或MIT许可证进行许可。Apache License, Version 2.0MIT license
除非您明确表示,否则根据Apache-2.0许可证定义的,您有意提交以包括在本包中的任何贡献,均应按上述方式双许可,没有任何额外条款或条件。

依赖项

~295–740KB
~18K SLoC