为 Scala 的 OSS 生态系统做出贡献

Scala 2 黑客指南

语言


本指南旨在帮助您从修复错误或将新功能实现到 Scala 夜间构建的想法,最终到包含您想法的 Scala 生产版本。

本指南涵盖了整个过程,从构思您的想法或错误修复到将其合并到 Scala 的过程。在整个过程中,我们将使用一个示例,说明您可能希望贡献的想法或错误修复。

对于首次贡献者来说,其他良好的起点包括 Scala 自述文件贡献者指南

运行示例

假设您特别喜欢 Scala 2.10.0 中引入的新字符串插值语言功能,并且您使用得非常频繁。

不过,有一个令人讨厌的问题,您偶尔会遇到:格式化字符串插值器 f 不支持 换行符令牌 %n

一种方法是转到 Scala 2 错误跟踪器,请求修复错误,然后无限期地等待修复。另一种方法是自行修补 Scala,并将修复提交到 Scala 存储库,希望它能进入后续版本。

注意:有几种类型的版本/构建。夜间构建每晚在固定时间生成。次要版本每隔几个月发布一次。主要版本通常每年发布一次。

1. 连接

有时,独自黑客攻击而不必与他人互动很有吸引力。然而,在 Scala 这样的大型项目中,可能有更好的方法。Scala 社区中有些人花了数年时间积累了有关 Scala 库和内部结构的知识。他们可能会提供独特的见解,更重要的是,在他们的领域提供直接的帮助,因此,与社区就您的新补丁进行沟通不仅有利,而且是推荐的。

通常,错误修复和新功能最初只是一个想法或实验,发布在 我们的论坛 上,以了解人们对您想要实现的内容的看法。精通 Scala 某些领域的通常会监控论坛和讨论室,因此您通常可以通过发布消息获得一些帮助。但最有效的联系方式是在您的消息中提及负责维护您希望为其做出贡献的 Scala 方面的负责人之一。

语言功能/库列表及其维护者的全名和 GitHub 用户名 在 Scala 仓库自述文件中

在我们的运行示例中,由于 Martin 是提交字符串插值 Scala 改进提案并为 Scala 2.10.0 实现此语言功能的人,因此他可能对了解该功能的新错误修复感兴趣。

如前所述,还必须选择一个合适的途径来讨论问题。通常,人们会使用 Scala 贡献者论坛,因为那里有专门用于讨论 Scala 系统的核心内部设计和实现的帖子类别。

在此示例中,该问题之前在(现已弃用)scala-user 邮件列表中讨论过,当时,我们会在 (现已弃用)scala-user 邮件列表 中发布有关我们问题的内容。

Posting to scala-user Response from Martin

现在我们获得了功能作者的批准,我们可以开始工作了!

2. 设置

破解 Scala 从为您的工作项创建一个分支开始。为了开发 Scala,我们使用 GitGitHub。本指南的这一部分提供了一个简短的演练,但如果您不熟悉 Git,那么首先熟悉 Git 可能更有意义。我们推荐

Fork

登录 GitHub,转到 https://github.com/scala/scala,然后单击页面右上角的 Fork 按钮。这将创建我们仓库的副本,作为您工作的备忘录。

如果您不熟悉 Git,请不要害怕搞砸 - 您无法破坏我们的仓库。

Fork scala/scala

克隆

如果一切顺利,您将被重定向到您自己的分支 https://github.com/user-name/scala,其中 username 是您的 GitHub 用户名。您可能会发现阅读 https://help.github.com/fork-a-repo/ 有帮助,其中涵盖了一些后续内容。然后,通过在命令行运行以下内容克隆您的存储库(即从 GitHub 拉取一个副本到您的本地计算机)

16:35 ~/Projects$ git clone https://github.com/xeno-by/scala
Cloning into 'scala'...
remote: Counting objects: 258564, done.
remote: Compressing objects: 100% (58239/58239), done.
remote: Total 258564 (delta 182155), reused 254094 (delta 178356)
Receiving objects: 100% (258564/258564), 46.91 MiB | 700 KiB/s, done.
Resolving deltas: 100% (182155/182155), done.

这将在本地创建一个名为 scala 的目录,其中包含您自己的存储库副本的克隆。您在此目录中所做的更改可以传播回您在 GitHub 上托管的副本,最终在您的补丁准备就绪后推送到 Scala。

分支

在开始进行更改之前,请务必创建您自己的分支。切勿在 master 分支上工作。想一个描述您计划进行的更改的名称。使用描述更改性质的前缀。从本质上讲,有两种更改:错误修复和新功能。

  • 对于错误修复,请使用 issue/NNNNticket/NNNN,以获取 Scala 错误跟踪器 中的错误 NNNN
  • 对于新功能,请使用 topic/XXX,以获取功能 XXX。使用在整个 Scala 项目的上下文中而不是仅对您个人有意义的功能名称。例如,如果您在 Scaladoc 中处理图表,请使用 topic/scaladoc-diagrams,而不是仅使用 topic/diagrams,这将是一个好的分支名称。

由于在我们的示例中,我们将修复现有错误 scala/bug#6725,因此我们将创建一个名为 ticket/6725 的分支。

16:39 ~/Projects/scala (master)$ git checkout -b ticket/6725
Switched to a new branch 'ticket/6725'

如果您不熟悉 Git 和分支,请阅读 Git Pro 书籍中的 分支章节

构建

克隆您的分支后的下一步是设置您的计算机以构建 Scala。

您需要以下工具

  • Java JDK。对于 2.13.x 及更高版本,基准版本为 8。对于本地开发,可以使用较高的 JDK 版本,但持续集成构建将针对基准版本进行验证。
  • sbt,一个交互式构建工具,通常用于 Scala 项目。无需手动获取 sbt - 建议的方法是下载 sbt-extras 运行程序脚本 并将其用作 sbt 的替代。从 Scala 存储库的根目录运行时,该脚本将下载并运行正确的 sbt 版本。
  • curl - 构建在 pull-binary-libs.sh 脚本中使用 curl 下载引导程序库。

macOS 和 Linux 构建应该可以正常工作。Windows 受支持,但可能存在问题。如果您遇到任何问题,请向 Scala 2 错误跟踪器 报告。

在克隆存储库的根目录下,使用单个命令 sbt dist/mkPack 即可构建 Scala。通常,进入 sbt shell 并从那里运行各种任务要比在命令提示符下启动 sbt some-task 来运行每个任务更有效率。

做好等待的准备 - 完整的“clean”构建需要 5 分钟以上,具体取决于你的机器(在内存较少的旧机器上需要更长时间)。在最近的笔记本电脑上,增量构建通常在 10-30 秒内完成。

IDE

没有一个编辑器可以作为使用 Scala 源代码的首选,因为每种可用工具都有其权衡利弊。

IntelliJ IDEA 有一个 Scala 插件,已知可以与我们的代码库配合使用。或者,你可以将 Visual Studio Code 与 Metals IDE 扩展 一起使用。这两种 Scala IDE 解决方案都提供导航、重构、错误报告功能和集成调试。请参阅 Scala 自述文件,了解如何将 IntelliJ IDEA 或 Metals 与 Scala 存储库一起使用。

还有其他备用编辑器,例如 Atom、Emacs、Sublime Text 或 jEdit。这些编辑器运行速度更快,对内存/计算的要求也更低,但缺少语义服务和调试功能。

我们认识到每个人对特定的 IDE/编辑器体验都有偏好,因此我们最终建议你根据自己的个人偏好进行选择。

3. 黑客

在针对你选择的主题进行黑客攻击时,你将修改 Scala,对其进行编译,并在相关的输入文件中对其进行测试。通常,你首先需要确保你的更改适用于一个小示例,然后通过运行全面的测试套件来验证是否没有任何内容被破坏。

我们将首先创建一个 sandbox 目录(./sandbox 在 Scala 存储库的 .gitignore 中列出),其中将保存一个测试文件及其编译结果。首先,让我们确保 该 bug 确实可以通过将一个简单的测试放在一起并使用我们使用 sbt 构建的 Scala 编译器对其进行编译和运行来重现。我们刚刚构建的 Scala 编译器位于 build/pack/bin 中。

17:25 ~/Projects/scala (ticket/6725)$ mkdir sandbox
17:26 ~/Projects/scala (ticket/6725)$ cd sandbox
17:26 ~/Projects/scala/sandbox (ticket/6725)$ edit Test.scala
17:26 ~/Projects/scala/sandbox (ticket/6725)$ cat Test.scala
object Test extends App {
  val a = 1
  val s = f"$a%s%n$a%s"
  println(s)
}
17:27 ~/Projects/scala/sandbox (ticket/6725)$ ../build/pack/bin/scalac Test.scala
17:28 ~/Projects/scala/sandbox (ticket/6725)$ ../build/pack/bin/scala Test
1%n1 // %n should've been replaced by a newline here

实现

现在,实现你的错误修复或新功能!

以下是一些在 Scala 开发中被证明有用的技巧和窍门

  • 使用 compile sbt 任务构建工作副本后,无需离开 sbt shell 的舒适环境即可试用:REPL 可用作 scala 任务,您还可以使用 scalac 任务运行编译器。如果您更喜欢在 sbt 外部运行 REPL,可以使用 dist/mkQuick 任务在 build/quick/bin 中生成脚本。
  • sbt 工作流也非常适合调试,因为您可以在您最喜欢的 IDE 中创建远程调试会话,然后使用以下命令激活下一次运行 scalascalac 任务的 JVM 选项
> set javaOptions in compiler := List("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8002")
> scalac test.scala
[info] Running scala.tools.nsc.Main -usejavacp test.scala
Listening for transport dt_socket at address: 8002
  • 另请参阅 Scala 自述文件,了解有关加快编译时间的提示。
  • 如果在引入更改或更新克隆后,您收到 AbstractMethodError 或其他链接异常,请尝试使用 clean 任务并重新构建。
  • 不要低估使用 println 打印调试信息的强大功能。在开始使用 Scala 时,我在调试器中花费了大量时间来弄清楚事情是如何工作的。然而,后来我发现基于打印的调试通常比四处跳转更有效。打印堆栈跟踪也很有用,可以了解执行流程,例如在某些操作发生之前执行了什么代码。使用 Trees 时,您可能需要使用 showRaw 来获取 AST 表示形式。
  • 您可以使用 sbt 中的 publishLocal 任务在本地发布新构建的 scala 版本。
  • 启用以下本地设置以加快您的工作流非常方便(将这些设置放在工作副本中的 local.sbt 中)
// skip docs for local publishing
publishArtifact in (Compile, packageDoc) in ThisBuild := false
// set version based on current sha, so that you can easily consume this build from another sbt project
baseVersionSuffix := s"local-${Process("tools/get-scala-commit-sha").lines.head.substring(0, 7)}"
// show more logging during a partest run
testOptions in IntegrationTest in LocalProject("test") ++= Seq(Tests.Argument("--show-log"), Tests.Argument("--show-diff"))
// if incremental compilation is compiling too much (should be fine under sbt 0.13.13)
// antStyle := true
  • 将宏添加到 Predef 对象是一项相当复杂的任务。由于引导,它使添加宏变得更加复杂。因此,这个过程更加复杂。复制 StringContext.f 本身添加的方式可能很有用。简而言之,您需要在 src/compiler/scala/tools/reflect/ 下定义您的宏,并在 Predef 中不提供实现(它将看起来像 def fn = macro ???)。现在您必须设置布线。将您的宏的名称添加到 src/reflect/scala/reflect/internal/StdNames.scala,向 src/reflect/scala/reflect/internal/Definitions.scala 添加所需链接,最后在 src/compiler/scala/tools/reflect/FastTrack.scala 中指定绑定。这是一个添加宏的示例。

在哪里可以找到文档

Scala 下的各个独立项目都有不同数量的文档

Scala 库

为 Scala 标准库做出贡献与处理您自己的库类似。

如果 Scala 标准库中某些特征/类/对象/方法等需要文档,维护人员通常会包含内联注释,描述其设计决策或实现事物的理由,如果它不是直接的。

Scala 标准库的一部分 Scala 集合框架比较复杂。您应该熟悉其架构,该架构记录在 Scala 集合架构 中。Scala 集合指南 更加通用,涵盖集合的同步部分。对于并行集合,还存在一个详细的 Scala 并行集合指南

Scala 编译器

有关 Scala 编译器内部工作原理的文档很少,并且大部分知识都是通过论坛 (Scala 贡献者 论坛)、聊天室(请参阅 Discord 上的 #scala-contributors)、工单或口口相传的方式传递的。但是,情况正在稳步改善。以下资源可能会有所帮助

  • Martin Odersky 的编译器内部视频 相当陈旧,但仍然非常有用。在这个三视频系列中,Martin 解释了编译器的一般架构以及前端的基础知识,前端后来成为 scala-reflect 模块的 API。
  • 反射文档 描述了用于表示 Scala 程序和定义在它们上面的操作的基本数据结构(如 TreeSymbolTypes)。由于编译器的很大一部分已经被分解出来并通过 scala-reflect 模块访问,因此反射所需的所有基础知识对于编译器都是相同的。
  • Scala 编译器专栏 包含有关 Scala 编译器中大多数类型化后阶段(即后端)的详尽文档。
  • Scala 贡献者,一个论坛,用于讨论 Scala 系统的核心内部设计和实现。
其他项目

Scaladoc 等工具也欢迎贡献。不幸的是,这些较小的项目开发人员文档较少。在这些情况下,最好的办法是直接探索代码库(通常包含文档作为内联注释)或写信给适当的维护者以获取指针。

插曲

为了修复我们感兴趣的错误,我们已经跟踪了StringContext.f插值器到MacroImplementations.scala中实现的宏。在那里我们注意到插值器只处理转换,而不处理%n之类的标记。看起来很容易修复。

18:44 ~/Projects/scala/sandbox (ticket/6725)$ git diff
diff --git a/src/compiler/scala/tools/reflect/MacroImplementations.scala b/src/compiler/scala/tools/
index 002a3fce82..4e8f02084d 100644
--- a/src/compiler/scala/tools/reflect/MacroImplementations.scala
+++ b/src/compiler/scala/tools/reflect/MacroImplementations.scala
@@ -117,7 +117,8 @@ abstract class MacroImplementations {
       if (!strIsEmpty) {
         val len = str.length
         while (idx < len) {
-          if (str(idx) == '%') {
+          def notPercentN = str(idx) != '%' || (idx + 1 < len && str(idx + 1) != 'n')
+          if (str(idx) == '%' && notPercentN) {
             bldr append (str substring (start, idx)) append "%%"
             start = idx + 1
           }

应用修复并运行sbt compile后,我们在sandbox/Test.scala中的简单测试用例开始工作!

18:51 ~/Projects/scala/sandbox (ticket/6725)$ cd ..
18:51 ~/Projects/scala (ticket/6725)$ sbt compile
...
[success] Total time: 18 s, completed Jun 6, 2016 9:03:02 PM
Total time: 18 seconds

18:51 ~/Projects/scala (ticket/6725)$ cd sandbox
18:51 ~/Projects/scala/sandbox (ticket/6725)$ ../build/pack/bin/scalac Test.scala
18:51 ~/Projects/scala/sandbox (ticket/6725)$ ../build/pack/bin/scala Test
1
1 // no longer getting the %n here - it got transformed into a newline

测试

为了防止你的更改在将来意外损坏,添加测试非常重要。我之前已经写了一个测试,这是一个好的开始,但还不够!除了我们新功能的明显用法之外,我们还需要涵盖特殊情况。

将测试添加到测试套件就像将它们移动到适当的目录一样简单

  • 应该成功编译但不需要执行的代码需要进入“pos”目录
  • 不应该编译的代码需要进入“neg”目录
  • 需要编译并由测试套件执行的代码需要放入 “run” 目录,并带有对应的 .check 文件,其中包含预期输出。如果 .check 文件的内容与测试在运行时产生的内容不同,则会收到测试失败。如果输出中的更改是您工作的预期结果,则可能不想手动更改 .check 文件。要让 partest 更改 .check 文件,请使用 --update-check 标志运行它,如下所示 ./test/partest --update-check path/to/test.scala。有关 partest 的更多信息,请参阅其 文档
  • 所有可以进行单元测试的内容都应放入 “junit” 目录
  • 基于属性的测试放入 “scalacheck” 目录

以下是更多测试提示

  • 如果您有多个测试,并且想要一个工具来仅运行符合某个正则表达式的测试,则可以使用 tools 目录中的 partest-ack./tools/partest-ack "dottype"。2.12 中已删除 partest-ack
  • 如果您想从 sbt 运行所有 scalacheck 测试,请使用 scalacheck/testOnly
  • 要在 sbt 中按名称运行 scalacheck 测试,请使用 scalacheck/testOnly <test1> ... <testN>,例如 scalacheck/testOnly scala.tools.nsc.scaladoc.HtmlFactoryTest
  • 如果您的测试以以下方式失败

      test.bc:
         [echo] Checking backward binary compatibility for scala-library (against 2.11.0)
         [mima] Found 2 binary incompatibiities
         [mima] ================================
         [mima]  * synthetic method
         [mima]    scala$package$Class$method(java.lang.String)Unit in trait
         [mima]    scala.package.Class does not have a correspondent in old version
         [mima]  * synthetic method
         [mima]    scala$package$AnotherClass$anotherMethod(java.lang.String)Unit in trait
         [mima]    scala.package.AnotherClass does not have a correspondent in old version
         [mima] Generated filter config definition
         [mima] ==================================
         [mima]
         [mima]     filter {
         [mima]         problems=[
         [mima]             {
         [mima]                 matchName="scala.package.Class$method"
         [mima]                 problemName=MissingMethodProblem
         [mima]             },
         [mima]             {
         [mima]                 matchName="scala.package.AnotherClass$anotherMethod"
         [mima]                 problemName=MissingMethodProblem
         [mima]             }
         [mima]         ]
         [mima]     }
         [mima]
    
       ...
       Finished: FAILURE
    

这意味着您的更改与指定版本向后或向前二进制不兼容(此检查由 迁移管理器 执行)。错误消息实际上说明了您需要修改 project/MimaFilters.scala 以消除错误。如果您在内部/实验 API 上收到此错误,则将建议的部分添加到配置中应该是安全的。否则,您可能希望针对此更改的目标是较新版本的 Scala。

验证

现在,为了确保我的修复不会破坏任何内容,我需要运行测试套件。Scala 测试套件使用 JUnitpartest,这是我们为测试 Scala 而编写的工具。分别运行 sbt testsbt partest 以运行所有 JUnit 和 partest 测试。partest(不是 sbt partest)还允许您使用通配符运行测试子集

18:52 ~/Projects/scala/sandbox (ticket/6725)$ cd ../test
18:56 ~/Projects/scala/test (ticket/6725)$ partest files/run/*interpol*
Testing individual files
testing: [...]/files/run/interpolationArgs.scala                      [  OK  ]
testing: [...]/files/run/interpolationMultiline1.scala                [  OK  ]
testing: [...]/files/run/interpolationMultiline2.scala                [  OK  ]
testing: [...]/files/run/sm-interpolator.scala                        [  OK  ]
testing: [...]/files/run/interpolation.scala                          [  OK  ]
testing: [...]/files/run/stringinterpolation_macro-run.scala          [  OK  ]
All of 6 tests were successful (elapsed time: 00:00:08)

4. 发布

开发完成后,是时候发布代码并提交补丁以供讨论并可能纳入 Scala。简而言之,这涉及

  1. 确保你的代码和提交信息质量很高,
  2. 在 GitHub 界面中单击几个按钮,
  3. 指定一位或多位审阅者,他们将查看你的拉取请求。

让我们更详细地了解这些要点。

提交

Git 在线手册中的Git 基础章节涵盖了此阶段的大部分基本工作流程。这里有两件事你应该知道

  1. 提交信息通常是了解几年前编写的代码作者意图的唯一方法。因此,编写高质量至关重要。你为所做的更改提供的上下文越多,未来维护人员了解你意图的机会就越大。查阅拉取请求政策以获取有关提交所需样式的更多信息。
  2. 保持 Scala 的 git 历史记录干净也很重要。因此,我们不会接受包含多个提交的错误修复的拉取请求。对于功能,可以有多个提交,但每次提交后所有测试都必须通过。要清理你的提交结构,你需要重写历史记录,使用 git rebase,以便你的提交针对 master 的最新修订版。

一旦你对自己的工作感到满意,与 master 同步并清理了提交,你就可以向中央 Scala 存储库提交补丁。在继续之前,请确保已将所有本地更改推送到 GitHub 上的分支。

19:22 ~/Projects/scala/test (ticket/6725)$ git add ../src/compiler/scala/tools/reflect/MacroImplementations.scala
19:22 ~/Projects/scala/test (ticket/6725)$ git commit
[ticket/6725 3c3098693b] SI-6725 `f` interpolator now supports %n tokens
 1 file changed, 2 insertions(+), 1 deletion(-)
19:34 ~/Projects/scala/test (ticket/6725)$ git push origin ticket/6725
Username for 'https://github.com': xeno-by
Password for 'https://[email protected]':
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 1.00 KiB, done.
Total 8 (delta 5), reused 0 (delta 0)
To https://github.com/xeno-by/scala
 * [new branch]            ticket/6725 -> ticket/6725

提交

现在,我们只需提交我们建议的补丁。导航到你在 GitHub 中的分支(对我来说,是 https://github.com/xeno-by/scala/tree/ticket/6725),然后单击拉取请求按钮,将你的补丁作为拉取请求提交给 Scala。如果你从未向 Scala 提交过补丁,则需要签署贡献者许可协议,可以在线完成,只需几分钟。

Submit a pull request

审核

提交拉取请求后,你需要选择一位审阅者(通常是你工作流程开始时联系的人),并在必要时准备好详细说明和调整你的补丁。在此示例中,我们选择了 Martin,因为我们在邮件列表中进行了非常愉快的交谈

SAssign the reviewer

合并

审阅者对你的代码感到满意后(通常会以 LGTM(“对我来说看起来不错”)表示),你的工作就完成了。请注意,成功的审核和合并之间可能存在差距,因为并非每位审阅者都拥有合并权限。在这种情况下,团队中的其他人会选取你的拉取请求并将其合并。因此,如果审阅者说“LGTM”,但你的代码没有立即合并,请不要感到困惑。

此页面的贡献者