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 在 值格式化 中
5,128,156 每月下载量
在 3,795 个 crates (237 直接使用) 中使用
240KB
5.5K SLoC
prettyplease::unparse
一个最简单的 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()
。特定于语言的代码将输入数据结构分解成一系列string
和break
标记,以及用于分组的begin
和end
标记。每个begin
–end
范围可以标识为“一致断行”或“不一致断行”。如果一个组是一致的断行,那么如果整个内容一行放不下,那么组中的每个break
标记都会得到一个换行符。例如,这适用于Rust结构字面量或函数调用的参数。如果一个组是不一致的断行,那么组中的string
标记会被贪婪地放置在一行中,直到没有空间为止,并且只在那些下一个字符串放不下的break
标记处断行。例如,这适用于Rust中花括号use
语句的内容。
Scan的任务是高效地累积关于组和断行的尺寸信息。对于每个begin
标记,我们计算到匹配的end
标记的距离,对于每个break
,我们计算到下一个break
的距离。该算法使用环形缓冲区来存储尺寸尚未确定的标记,环形缓冲区的最大尺寸由目标行长度限制,不会无限增长,无论输入流中的嵌套有多深。这是因为一旦一个组足够大,精确的尺寸就不再影响断行决策,我们可以将其有效地视为“无穷大”。
Print的任务是使用尺寸信息来高效地为每个begin
标记分配“断行”或“不断行”的状态。此时,输出可以通过连接字符串标记并在断行组中的break
标记处断行来轻松构建。
利用这些原始功能(即巧妙地放置全有或全无的一致断行和贪婪的不一致断行)以生成与rustfmt兼容的格式化,适用于Rust的语法树的所有节点,这是一个有趣的挑战。
以下是Rust标记输入到美化打印算法的视觉表示。一致断行的begin
—end
对用«
»
表示,不一致断行用‹
›
表示,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.0或MIT license。除非您明确表示,否则根据Apache-2.0许可证定义的,您有意提交以包括在本包中的任何贡献,均应按上述方式双许可,没有任何额外条款或条件。
依赖项
~295–740KB
~18K SLoC