14个版本
0.2.20 | 2024年5月24日 |
---|---|
0.2.16 | 2023年12月22日 |
0.2.15 | 2023年10月8日 |
0.2.11 | 2023年6月8日 |
0.1.25 | 2023年3月21日 |
#761 在 开发工具
每月 1,285 次下载
用于 3 个crate(直接使用2个)
230KB
5.5K SLoC
prettyplease::unparse
一个最小的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片段(如$: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),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
之间的距离。算法使用一个 ringbuffer 来存储尺寸尚未确定的符号。ringbuffer 的最大尺寸由目标行长度限制,不会无限增长,无论输入流中嵌套有多深。这是因为一旦一个组足够大,精确的尺寸不再影响换行决策,我们可以将其有效地视为“无限”。
Print 的任务是使用尺寸信息高效地为每个 begin
符号分配“断行”或“未断行”状态。到那时,输出就可以通过连接 string
符号并在断行组内的 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·»)›·
» }·
»}·
论文中描述的算法并不足以生成格式良好的 Rust 代码,其本地与 rustfmt 风格不可区分。原因在于,在论文中,假设完整的非空白内容与换行决策无关,Scan 和 Print 只控制空白(空格和换行)。但在 rustfmt 习惯性格式化的 Rust 中,情况并非如此。尾随逗号就是一个例子;标点符号只在周围组的断行与未断行状态已知之后才知道。
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.0版本或MIT许可证中选择一个。除非您明确声明,否则根据Apache-2.0许可证定义的,您有意提交并包含在本库中的任何贡献都应双授权如上,没有额外的条款或条件。
依赖关系
~255–680KB
~16K SLoC