Rust Logo Rust 博客
  • Rust
  • 安装
  • 学习
  • 工具
  • 治理
  • 社区

文档内链接即将稳定

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

我们很高兴地分享,文档内链接即将稳定!

文档内链接是 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 的所有帮助!

获取帮助!

  • 文档
  • 联系 Rust 团队

条款和政策

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

社交媒体

mastodon logo twitter logo youtube logo discord logo github logo

RSS

  • 主博客
  • “Rust 内部”博客
由 Rust 团队维护。发现错别字?请在此处发送修复!