我们很高兴地分享,文档内链接即将稳定!
文档内链接是 rustdoc
的一个特性,它允许您通过名称链接到“项”——函数、类型等等,而不是硬编码的 URL。即使您的类型在不同的模块或 crate 中重新导出,这也可以让您拥有准确的链接。这是一个简单的例子
/// Link to [`f()`]
pub struct S;
pub fn f() {}
文档内链接已经存在一段时间了,最早可以追溯到 2017 年!它们在 nightly
版本中无需任何标志即可使用(因此在 docs.rs 上也可以使用),因此您可能会惊讶地听到它们尚未稳定。现在改变的是,它们将在稳定的 Rust 版本上可用,这也意味着我们对实现更有信心,并强烈建议使用它们。我们建议您切换您的库以使用文档内链接,这将修复重新导出类型和链接到不同 crate 的断开链接。我们希望在未来使用 cargo fix
添加对此过程自动化的支持。
文档内链接的历史
我(Manish)和 QuietMisdreavus 在 2017 年 12 月开始研究它们。Mozilla 在发布 Firefox Quantum 后给了整个公司几周的假期,当时我正在孟买探亲。这意味着我有相当多的空闲时间,而且我们处于完全相反的时区。QuietMisdreavus 已经研究这个特性一段时间了,但不太熟悉 rustc 的路径解析代码,所以我决定帮忙。我们最终合作了那几周:白天我会编写一些代码,晚上与 QuietMisdreavus 讨论,然后交给她连夜继续。这是一次很棒的体验,在开源项目中合作真的很有趣!最终形成了一个 包含 46 个提交的 pull request,其中包含我们两人的提交。
不幸的是,我们当时无法稳定该功能。主要的障碍是 跨 crate 重新导出,例如以下情况
// Crate `inner`
/// Link to [`f()`]
pub struct S;
pub fn f() {}
// outer crate
pub use inner::S;
rustdoc
处理重新导出的方式是就地渲染重新导出,解析并渲染所有 markdown。这里的问题是,当文档化 outer
时,rustdoc
无法访问 inner::S
的本地作用域信息,也无法解析 f()
。
这些链接是文档内链接的最初动机,所以如果我们不能让它们工作,那么稳定它们就没有多大意义了!它们也有可能悄悄地断开——文档在您构建它时会工作,但是您的 API 的任何用户都可以重新导出您的类型,并导致链接断开。
当时,持久化本地作用域信息,以便下游 crate 上的 rustdoc
调用可以访问它们,需要在编译器上进行大量工作。这是编译器团队无论如何都希望完成的工作,但这有很多工作要做,而且我们俩都没有带宽来做,所以我们提交了一个错误,然后继续我们的工作。
发生了什么变化?
在六月初,我(Jynn)厌倦了无法使用文档内链接。我开始调查这个问题,看看是否有解决方法。它被标记为 E-hard
,所以我并没有期待奇迹,但我认为我至少可以开始着手解决它。
事实证明,实现中存在一个简单的问题——它假设所有项都在当前的 crate 中!显然,情况并非总是如此。修复方案最终非常简单,我可以用它作为我对 rustdoc 的第一个贡献来实现。
来自 Manish 的说明: 实际上,当我们编写该特性时,DefId
和 LocalDefId
之间的区别不存在,并且代码只会根据解析器当前的内部作用域(它只能在当前 crate 中,因为这是解析器当时拥有的唯一作用域信息)来解析路径。然而,随着时间的推移,编译器 获得了存储和查询依赖项解析范围的能力。我们从未注意到,并继续认为有一项大型工作在阻碍稳定。
然而,我的解决方案有一个小问题:在某些精心制作的输入上,它会崩溃
#![feature(decl_macro)]
fn main() {
|| {
macro m() {}
};
}
thread 'rustc' panicked at 'called `Option::unwrap()` on a `None` value', /home/jyn/src/rust/src/librustc_hir/definitions.rs:358:9
HirIds 和 DefIds 和树,我的天!
(如果您对 Rust 编译器的内部结构不感兴趣,请随意跳过此部分。)
上面的错误是由于一个名为 everybody_loops
的 pass 引起的。编译器“pass”是对源代码的一种转换,例如 查找没有文档的项。everybody_loops
pass 将上面的代码变成
fn main() {
{
macro m { () => { } }
}
loop { }
}
作为我解决跨 crate 项的更改的一部分,我需要知道第一个父模块,这样我才能知道哪些项在作用域内。但是请注意,在 everybody_loops
之后,闭包消失了!崩溃发生的原因是 rustdoc
试图访问 rustc
认为不存在的闭包(在编译器术语中,它将闭包的 DefId
(跨 crate 工作)转换为 HirId
,HirId
特定于当前 crate,但包含更多信息)。
为什么这很难?
事实证明,这是一个巨大的兔子洞。everybody_loops
最早是在 2017 年引入的,以解决另一个长期存在的问题:rustdoc
不知道如何处理条件编译。它允许 rustdoc(以及标准库)忽略函数体中的类型和名称错误。这允许在同一主机上记录 Linux 和 Windows API,即使实现通常会崩溃。如上所示,它的工作方式是将每个函数体变成 loop {}
- 这始终有效,因为 loop {}
的类型是 !
,它可以强制转换为任何类型!
但是,正如我们上面看到的,这种转换破坏了 rustdoc。此外,它还导致了许多问题和其他问题。
所以我摆脱了它!这就是 不要运行 everybody_loops。这是我为 rustc 所做的最大 PR,希望也是我永远会做的最大 PR。问题是 libstd 的错误并没有消失 - 如果有的话,自 2017 年以来它已经扩大了。我想出的技巧是,与其运行类型检查并尝试将代码重写为有效代码,不如根本不在函数体中运行类型检查!这既减少了工作量,也更接近 rustdoc 想要的语义。特别是,它永远不会导致崩溃 rustdoc
的无效状态。
后果:做好事没好报
在 PR 合并大约一个月后,rustdoc 收到了一个错误报告:async-std
的文档无法在 nightly 频道上构建。他们的代码看起来像 以下
mod windows {
pub trait WinFoo {
fn foo(&self) {}
}
impl WinFoo for () {}
}
#[cfg(any(windows, doc))]
use windows::*;
mod unix {
pub trait UnixFoo {
fn foo(&self) {}
}
impl UnixFoo for () {}
}
#[cfg(any(unix, doc))]
use unix::*;
async fn bar() {
().foo()
}
特别是,请注意在 cfg(doc)
下,两个 trait 都将在具有相同方法的作用域内,因此 .foo()
使用哪个是不明确的。这正是旨在通过不运行类型检查来解决的问题。不幸的是,由于它在 async fn
中使用,类型检查仍在运行;bar
解糖为以下形式的闭包
fn bar() -> impl Future<Output = ()> {
async {
().foo()
}
}
因为该函数返回 impl Future
,所以需要对函数体进行类型检查,以推断函数的返回类型。这正是 rustdoc
不想做的事情!
实现的简陋的“修复”是不推断函数的类型 - rustdoc 不关心确切的类型,只关心它实现的 trait。这是一个非常简陋的修复方案,以至于有一个 问题要解决它。
稳定文档内链接
既然跨 crate 重新导出可以工作了,那么就没有什么可以阻止稳定文档内链接了!有一些少量清理PR,但在大多数情况下,稳定化的路径似乎很明确。
与此同时,我一直在研究对文档内链接的各种改进
特别感谢很多人挺身而出,帮助将标准库转换为内部文档链接。非常感谢 @camelid, @denisvasilik, @poliorcetics, @nixphix, @EllenNyan, @kolfs, @LeSeulArtichaut, @Amjad50, 和 @GuillaumeGomez 的所有帮助!