改进 OCaml 文档工具链
上周,我们 发布 了一个新的 OCaml 文档生成器的 alpha 版本,codoc 0.2.0。在 2014 年 OCaml 研讨会演示文稿中 (摘要,幻灯片,视频),我们提到了文档的“模块墙”,这次尝试要解决它。要试用它,只需按照该存储库中 README 中的说明进行操作,或者 浏览一些当前工具默认输出的样本。请记住,codoc 及其组成库仍在紧张开发中,尚未完成所有功能。
codoc
的目标是提供一套广泛有用的工具来生成 OCaml 文档。特别是,我们正在努力
- 涵盖 OCaml 的所有语言特性
- 提供准确的名称解析和链接
- 支持不同包之间的交叉链接
- 公开我们用于构建
codoc
的组件的接口 - 为工具本身提供一个无魔法的命令行界面
- 减少外部依赖关系,并与其他工具默认集成
我们还没有在工具堆栈的所有层面上都实现这些目标,但我们正在接近。codoc
0.2.0 现在可以使用了(尽管在某些方面,比如默认 CSS,还有些粗糙)。这篇文章概述了新系统的体系结构,以便更容易理解构建它的设计决策。
文档的五个阶段
从 OCaml 源代码生成文档有五个阶段。这里我们将描述每个阶段在过去(使用 OCamldoc)如何处理,现在(使用我们目前的原型)如何处理,以及将来(使用我们正在开发的工具的最终版本)如何处理。
将注释与定义关联
第一步是将 .ml
或 .mli
文件中的各种文档注释与它们对应的定义相关联。
过去
将注释与定义关联由 OCamldoc 工具处理,该工具分两步完成此操作。首先,它使用常规的 OCaml 解析器或 camlp4 解析文件,就像在正常编译中一样。它使用第一步中的语法树,然后重新解析文件,查找注释。第二次解析由语法树中的位置信息引导;例如,如果有一个定义在第 5 行结束,那么 OCamldoc 将从第 6 行开始查找要附加到该定义的注释。
用于附加注释的规则非常复杂,并且依赖于空格。这使得使用单个解析器解析文件并附加注释变得很困难。特别是,很难以一种不会对 camlp4 扩展造成很多问题的方式做到这一点。这就是 OCamldoc 分两步完成此过程的原因。
这种两步法的缺点是它假设任何预处理器的输入都是可以被编译器/工具创建文档合理读取的东西,但这并不总是正确的。
现在
我们目前的原型在编译器本身内部将注释与定义关联起来。这依赖于对 OCaml 编译器的补丁 (GitHub 上的拉取请求 #51)。注释关联由 -doc
命令行标志激活。它使用(重新编写版本的)与 OCamldoc 当前使用的相同两步算法。然后将注释作为属性附加到语法树中的适当节点。这些属性会通过类型检查器传递,并出现在 .cmt
/.cmti
文件中,其他工具可以在这些文件中读取它们。
将来
我们打算放弃 OCamldoc 采用的两步法。为此,我们需要简化将注释与定义关联的规则。一个建议是使用与属性相同的规则,但这似乎过于限制。因此,我们希望采用的方法是尽可能地接近 OCamldoc 当前支持的方法,但禁止一些更模棱两可的情况。例如,
val x : int
(** Is this for x or y? *)
val y : float
可能不会在我们最终版本中得到支持。我们会注意了解这些设计决策的影响,并且希望找到一个可靠的未来解决方案。通过避免两步法,应该可以安全地始终打开注释关联,而不需要 -doc
命令行标志。
解析注释的内容
将文档注释与定义关联后,您必须解析这些注释的内容。
过去
OCamldoc 解析注释的内容。
现在
在我们目前的原型中,注释的内容在编译器中被解析,以便 .cmt
/.cmti
文件中可用的文档属性包含文档的结构化表示。
将来
我们打算将解析文档注释的内容与编译器分离。这意味着文档将作为字符串存储在 .cmt
/.cmti
文件中,并由外部工具解析。这将允许文档语言(及其解析器)比编译器的发布周期更快地发展。
用类型和文档表示编译单元
存储在 .cmt
/.cmti
文件中的类型化语法树不是一个方便的表示形式,用于从中生成文档,因此下一步是将语法树和注释转换为某种合适的中间形式。特别是,这允许统一处理 .cmt
文件和 .cmti
文件。
过去
OCamldoc 从语法树、类型化语法树和它在早期阶段找到和解析的注释中生成中间形式。对非类型化和类型化语法树的需求是历史遗留问题,现在已经不再需要了。
现在
我们目前的原型在 doc-ock 库中创建了一个中间形式。此形式目前可以从 .cmti
文件或 .cmi
文件创建。.cmi
文件不包含完整文档所需的信息,但如果您无法使用 .cmti
文件,可以使用它们生成部分文档。
此中间形式可以使用 doc-ock-xml 序列化为 XML。
将来
在最终版本中,doc-ock 也将支持读取 .cmt
文件。
解析引用
获得文档表示形式后,您需要解析所有路径和引用,以便链接可以指向正确的位置。例如,
(* 此类型由 {!Foo} 使用 *) type t = Bar.t
路径 Bar.t
和引用 Foo
必须解析,以便文档可以包含指向相应定义的链接。
如果您要为大量包生成文档,可能存在多个名为 Foo
的模块。因此,能够确定引用指的是哪一个 Foo
非常重要。
与大多数语言不同,由于 OCaml 的强大模块系统,解析路径可能非常困难。例如,考虑以下代码
module Dep1 : sig
module type S = sig
class c : object
method m : int
end
end
module X : sig
module Y : S
end
end
module Dep2 :
functor (Arg : sig module type S module X : sig module Y : S end end) ->
sig
module A : sig
module Y : Arg.S
end
module B = A.Y
end
type dep1 = Dep2(Dep1).B.c;;
在这里,它看起来像 Dep2(Dep1).B.c
将由函子 Dep2
的子模块 B
中的类型定义 c
定义。但是,Dep2.B
的类型实际上取决于 Dep2
的 Arg
参数的类型,因此实际的定义是 Dep1
模块的模块类型 S
中的类定义。
过去
OCamldoc 使用非常简单的基于字符串的查找进行解析。这并非旨在处理模块名称不唯一的项目集合。它也不足以处理 OCaml 模块系统的更高级用法(例如,它无法解析上面示例中的路径 Dep2(Dep1).B.c
)。
现在
在我们目前的原型中,路径和引用解析由 doc-ock 库执行。实现相当于 OCaml 模块系统的一个重新实现,它跟踪生成准确路径和引用所需的附加信息(它也是延迟的,以提高性能)。该系统使用 .cmti
/.cmi
文件提供的摘要来解析对其他模块的引用,而不是仅仅依赖于模块的名称。
将来
doc-ock-lib 仍然有一些路径处理不正确,这些路径将得到修复,但总的来说,最终版本将与当前原型相同。
生成输出
最后,您已准备好使用这些工具生成一些输出。
过去
OCamldoc 支持多种输出格式,包括 HTML 和 LaTeX。它还包括对称为“自定义生成器”的插件的支持,允许用户添加对其他格式的支持。
现在
codoc
目前仅支持 HTML 和 XML 输出,尽管一旦接口稳定下来,额外的输出格式(如 JSON)应该很容易添加。codoc
为跟踪包层次结构、文档问题和分层本地化配置定义了一个文档索引 XML 格式。
codoc
还定义了一个可脚本化的命令行界面,允许用户访问其内部文档阶段:提取、链接和渲染。最新关于如何使用 CLI 的说明可以在 README 中找到。我们提供一个 OPAM 远程,其中包含驱动新文档引擎所需的所有新库和编译器补丁的工作版本。
将来
如前所述,codoc 及其组成库 doc-ock-lib 和 doc-ock-xml 仍在紧张开发中,尚未完成所有功能。值得注意的是,还有一些重要的未解决问题
- 类和类类型文档没有生成 HTML。 (问题 codoc#9)
- CSS 表现不佳。(codoc#27 问题)
- codoc HTML 不理解
--package
。(codoc#42 问题) - opam doc 过于侵入式(暂时用于演示目的;由 (codoc#48 问题) 跟踪)
- 文档语法错误未在正确的阶段或足够明显的报告。(codoc#58 问题)
- 字符集处理不正确 (doc-ock-lib#43 问题)
- -pack 和 cmt 提取不受支持 (doc-ock-lib#35 问题 和 doc-ock-lib#3 问题)
- 包含/替换不受支持 (doc-ock-lib#2 问题)
我们非常乐意在 https://github.com/dsheets/codoc/issues 接收错误报告和补丁。对于更广泛的建议、评论、投诉和讨论,请加入我们 Platform 邮件列表。我们希望您能告诉我们您的想法,并帮助我们构建下一代文档工具,为我们的社区提供良好的服务。