Rust Logo Inside Rust Blog
  • Rust
  • 安装
  • 学习
  • 工具
  • 治理
  • 社区

文档内链即将稳定

2020年9月17日 · Manish Goregaokar 和 Jynn Nelson 代表 rustdoc 团队

我们很高兴地宣布,文档内链(intra-doc links)即将稳定!

文档内链是 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个提交的拉取请求。

不幸的是,当时我们未能稳定这个特性。主要的障碍是跨 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` 的调用可以访问,需要在编译器上做大量工作。这是编译器团队无论如何都希望完成的工作,但这工作量很大,我们俩都没有足够的精力来完成,所以我们提交了一个 bug,然后各自继续其他工作了。

发生了什么变化?

六月初,我(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,后者特定于当前 crate 但包含更多信息)。

为什么这很难?

这原来是一个巨大的“兔子洞”(复杂问题)。everybody_loops 早在2017年就被引入,目的是解决另一个长期存在的问题:rustdoc 不知道如何处理条件编译。它允许 rustdoc(以及标准库)忽略函数体中的类型和名称错误。这使得在同一主机上可以同时文档化 Linux 和 Windows API,尽管它们的实现通常是无法编译的。如上所示,它的工作原理是将每个函数体转换为 loop {}——这总是有效的,因为 loop {} 的类型是 !,它可以强制转换为任何类型!

正如我们上面看到的,这种转换破坏了 rustdoc。此外,它还导致了许多其他问题(链接指向的都是各种问题或相关的 PR)。

所以我把它移除了!这就是“不要运行 everybody_loops”的拉取请求。这是我提交给 rustc 的最大 PR,希望也是我提交的最后一个这么大的 PR。问题是 libstd 中的错误并没有消失——如果说有什么变化,那就是自2017年以来错误反而增加了。我提出的一个“权宜之计”(hack)是,与其运行类型检查并尝试将代码重写成有效形式,不如根本不在函数体中运行类型检查!这既减少了工作量,也更符合 rustdoc 的期望语义。特别是,它不会导致那些导致 rustdoc 崩溃的无效状态。

后果:好心没好报

在 PR 合并大约一个月后,rustdoc 收到了一个 bug 报告: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() 来说,使用哪个 trait 是模糊不清的。这正是通过不运行类型检查来解决的问题。不幸的是,由于它用于 async fn,类型检查仍然在运行;bar 会被解糖(desugars)为以下形式的闭包

fn bar() -> impl Future<Output = ()> {
    async {
        ().foo()
    }
}

因为函数返回 impl Future,这需要对函数体进行类型检查以推断出函数的返回类型。这正是 rustdoc 不想做的事情!

实现的临时性“修复”是根本不推断函数的类型——rustdoc 不关心确切的类型,只关心它实现的 trait。这只是一个临时方案(hack),因此有一个issue 专门用于修复它。

稳定化文档内链

既然跨 crate 的重新导出已经工作了,文档内链的稳定化就没有太多障碍了!还有一些清理的 PR,但总体而言,通往稳定化的道路似乎已经明朗。

与此同时,我一直在改进文档内链的各个方面

  • 解析关联项
  • 修复实现中的各种错误(链接指向各个修复 PR)
  • 在整个标准库中使用文档内链
  • 检测更多链接歧义的情况
  • 移除那些只会分散文档注意力的消歧符
  • 改进链接解析失败时的错误消息

特别是,有很多人站出来帮助将标准库转换为文档内链。在此非常感谢 @camelid、@denisvasilik、@poliorcetics、@nixphix、@EllenNyan、@kolfs、@LeSeulArtichaut、@Amjad50 和 @GuillaumeGomez 的所有帮助!

获取帮助!

  • 文档
  • 联系 Rust 团队

条款和政策

  • 行为准则
  • 许可协议
  • 标志政策和媒体指南
  • 安全披露
  • 所有政策

社交媒体

mastodon logo Bluesky logo youtube logo discord logo github logo

RSS

  • 主博客
  • “Inside Rust”博客
由 Rust 团队维护。发现拼写错误?在这里提交修改!