Sorbet
本仓库包含 Sorbet,一个专为 Ruby 设计的快速、强大的类型检查器。 它旨在通过渐进式类型轻松集成到现有代码库中,并能快速 返回错误和建议。
本 README 包含专门针对向 Sorbet 贡献代码的文档。你可能还想:
- 阅读 公开的 Sorbet 文档
- 甚至 编辑文档
- 观看我们关于 Sorbet 的 演讲
- 在线试用 Sorbet 游乐场
如果你在 Stripe 工作,你可能还想查看 http://go/types/internals 以获取 关于 Stripe 特定开发工作流程和历史 Stripe 背景的文档。
目录
- Sorbet 面向用户的设计原则
- 快速入门
- 学习 Sorbet 的工作原理
- 构建 Sorbet
- 运行 Sorbet
- 运行测试
- 针对 pay-server 测试 Sorbet
- 编写测试
- 调试
- 编写文档
- 编辑器与环境
Sorbet 面向用户的设计原则
在项目初期,我们定义了一些关于使用 Sorbet 体验的准则。
-
显式
我们愿意编写注解,并且实际上认为它们 是有益的;它们使代码更具可读性和可预测性。我们旨在 帮助读者,也帮助编写者。
-
感觉有用,而非负担
虽然它是显式的,但我们致力于使其简洁。 这体现在多个方面:
- 错误信息应清晰明了
- 冗余度应通过更高的安全性来补偿
-
尽可能简单,但足够强大
总体而言,我们并不强烈信奉超级复杂的类型 系统。它们有其适用之处,我们需要相当多的 表达能力来建模(足够的)真实 Ruby 代码,但在其他条件相同的情况下,我们希望更简单。我们相信这样的系统 扩展性更好,并且——最重要的是——更容易让我们的用户 学习和理解。
-
与 Ruby 兼容
特别是,我们不希望引入新的语法。现有的 Ruby 语法意味着 我们可以利用我们现有的大部分工具(编辑器等)。此外, Sorbet 的目标是逐步改进现有的 Ruby 代码库。没有 新语法使其更容易与现有工具兼容。
-
可扩展
在所有维度上:执行速度、协作者数量、代码行数、 代码库年龄。我们在大型 Ruby 代码库中工作,而且它们只会 变得更大。
-
可逐步采用
为了能够大规模地实现采用,我们不能要求每个团队或 项目一次性全面采用 Sorbet。Sorbet 需要支持团队以不同的 节奏采用它。
快速入门
-
安装依赖项
brew install bazel autoconf coreutils parallel
-
克隆此仓库
git clone https://github.com/sorbet/sorbet.gitcd sorbet
-
构建 Sorbet
./bazel build //main:sorbet --config=dbg
-
运行 Sorbet!
bazel-bin/main/sorbet -e "42 + 'hello'"
学习 Sorbet 的工作原理
我们已在单独的文档中记录了 Sorbet 的内部机制。 请交叉参考该文档和本文,以了解 Sorbet 的工作原理以及如何 对其进行修改!
此外,还有一场在线演讲,介绍了 Sorbet 的高层架构以及 其速度快的原因:
构建 Sorbet
有多种方式可以构建 sorbet。以下是最常见的方式:
./bazel build //main:sorbet --config=dbg
这将构建一个可执行文件 bazel-bin/main/sorbet(参见下文“运行 Sorbet”)。
构建 sorbet 时,你可以传递许多选项:
--config=dbg- 开发环境中最常见的构建配置。
- 提供详细的堆栈跟踪,运行所有 ENFORCE 检查。
--config=sanitize- 链接额外的 sanitizer,特别是:UBSan 和 ASan。
- 捕获大多数内存和未定义行为错误。
- 二进制文件体积显著增大且运行速度变慢。
--config=debugsymbols- (由
--config=dbg包含)调试符号,不包含其他内容。
- (由
--config=forcedebug- 使用更多内存,但报告更多的健全性检查。
--config=static-libs- 强制使用静态链接(Sorbet 默认使用动态链接以加快 构建时间)。
- Sorbet 在发布构建中已经使用了此选项(见下文)。
--config=release-mac和--config=release-linux- 我们提供给用户的精确发布配置。
无论是否提供上述任何标志,你都可以为任何构建 启用优化:
-c opt- 启用
clang优化(即,-O2)
- 启用
这些参数并非互斥。例如,调试时常见的组合是
--config=dbg --config=sanitize
在 .bazelrc 中,你可以了解所有这些选项(以及其他选项)的含义。
常见编译错误
(Mac) Xcode version must be specified to use an Apple CROSSTOOL
此错误通常发生在 Xcode 升级之后。
必须安装开发者工具,必须接受 Xcode 许可,并且 你当前激活的 Xcode 命令行工具目录必须指向已安装的 Xcode 版本。
以下命令应该可以解决问题:
# Install command line tools
xcode-select --install
# Ensure that the system finds command line tools in an active Xcode directory
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
# Accept the Xcode license.
sudo xcodebuild -license
# Clear bazel's cache, which may contain files generated from a previous
# version of Xcode command line tools.
bazel clean --expunge
(Mac) fatal error: 'math.h' file not found(或其他系统头文件)
在 Mac 上,当 /usr/include 文件夹缺失时,可能会发生此错误。
解决方案是通过以下软件包安装 macOS 头文件:
macOS Mojave:
open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
macOS Catalina:
sudo ln -s /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/* /usr/local/include/
运行 Sorbet
在表达式上运行 Sorbet:
bazel-bin/main/sorbet -e "1 + false"
在文件上运行 Sorbet:
bazel-bin/main/sorbet foo.rb
运行 bazel-bin/main/sorbet --help 会显示许多选项。以下是
贡献者常用的选项:
-p <IR>- 要求 sorbet 打印出任何给定的中间表示。
- 请参阅
--help了解<IR>的可用值。
--stop-after <phase>- 当后续阶段存在 bug 且您希望提前退出以 进行调试时,此选项很有用。
-v、-vv、-vvv- 显示
logger输出(增加详细程度)
- 显示
--max-threads=1- 用于确定您是否正在处理并发 bug。
--wait-for-dbg- 将在启动时冻结 Sorbet 并等待调试器附加
- 当您无法控制进程启动时(LSP),此选项很有用
运行测试
要运行所有测试:
bazel test //... --config=dbg
(//... 字面意思是“所有目标”。)
要运行专为加快迭代和开发速度而精选的测试子集, 请运行:
bazel test test --config=dbg
请注意,在 Bazel 术语中,第二个测试是 //test:test 的别名,因此我们在这里玩了一点小花样。
默认情况下,所有测试输出都会写入文件。若要同时将其打印到屏幕上:
bazel test //... --config=dbg --test_output=errors
如果任何测试失败,你将看到打印出的两条信息:
1. //test:test_testdata/resolver/optional_constant
2. /private/var/tmp/.../test/test_testdata/resolver/optional_constant/test.log
- 测试的目标(如果你希望再次使用
bazel test <target>仅运行此测试) - 包含测试输出的(可执行)文件
要查看失败的输出,可以:
- 使用
--test_output=errors标志重新运行bazel test - 复制/粘贴
*.log文件并运行它(输出将在less中打开)
针对 pay-server 测试 Sorbet
这特定于在 Stripe 为 Sorbet 做贡献。
如果你在 Stripe 并希望针对 pay-server 测试你的分支,请参阅 http://go/types/local-dev。
编写测试
我们通过向 test/ 目录的子文件夹中添加文件来编写测试。
各个子文件夹是“魔法”的;每个包含特定类型的测试。
我们致力于使我们的测试完全可复现。
C++ 注意:在 C++ 中,哈希函数仅要求在同一程序的单次执行中, 对相同输入产生相同的结果。
因此,我们期望所有用户可见的输出都使用从一次运行到下一次运行 稳定的键进行显式排序。
测试 Sorbet 有许多方法,有些“更好”于其他。我们按从最优选到最不优选 的顺序在下方列出它们。并且我们始终偏好某些测试胜过没有测试!
test_corpus 测试
第一种测试可以称为 test_corpus 测试或 testdata 测试,分别基于测试框架的名称或包含这些测试的文件夹的名称。
要创建 test_corpus 测试,请在任意文件夹深度下,将任意文件 <name>.rb 添加到 test/testdata。该文件必须满足以下任一条件:
- 完全通过类型检查,或
- 仅在带有注释标记的行上抛出错误(见下文)。
要标记某行应存在错误,请在行尾追加 # error: <message>(<message> 必须与抛出的错误消息匹配)。如果该行存在多个错误,请在其正下方单独一行添加一个 # error: <message>。
错误检查可以选择指向字符范围而非整行:
1 + '' # error: `String` doesn't match `Integer`
rescue Foo, Bar => baz
# ^^^ error: Unable to resolve constant `Foo`
# ^^^ error: Unable to resolve constant `Bar`
您可以使用以下命令运行此测试:
bazel test //test:test_PosTests/testdata/path/to/<name>
期望测试
每个 test_corpus 测试都可以通过可选地创建任意数量的 <name>.rb.<phase>.exp 文件(其中 <name> 与该测试对应的 ruby 文件名称匹配)来转换为期望测试。这些文件包含内部数据结构的格式化表示,其内容应与 -p <phase> 打印的内容一致。快照必须与运行 sorbet -p <phase> <name>.rb 生成的输出完全匹配,测试才能通过。
你可以使用以下命令运行此测试:
bazel test //test:test_PosTests/testdata/path/to/<name>
以某个前缀和 __ 开头的文件将一起运行。例如,
foo__1.rb 和 foo__2.rb 将作为测试 foo 一起运行。如果此类文件
集合关联有 *.exp 文件,则 *.exp 文件必须遵循模式
<name>.<phase>.exp,其中 <name> 不包含
__*.rb 后缀。因此,foo__1.rb 和 foo__2.rb 将具有类似
foo.<pass>.exp 的 exp 文件。
另一个例外:对于 package-tree exp 测试,无论测试名称如何,
文件名始终为
pass.package-tree.exp。
CLI 测试
添加到 test/cli/ 的任何文件夹 <name> 都会成为一个测试。
该文件夹应包含一个可执行的文件 test.sh。
运行时,其输出将与该文件夹中的 test.out 进行比较。
我们的 bazel 配置将生成两个目标:
bazel run //test/cli:test_<name>将执行.sh文件bazel test //test/cli:test_<name>将执行.sh并将其与.out文件中的内容进行比对。
脚本在 Bazel 内部运行,因此它们将从工作区顶部执行,
并可以通过从根目录开始的路径访问源文件和构建目标。特别是,编译后的 sorbet 二进制文件位于
main/sorbet 下。
LSP 测试
大多数 LSP 测试是带有额外 LSP 特定注释的期望测试。
它们主要包含在 test/testdata/lsp 中,但 test/testdata 中的所有文件
都在 LSP 模式下进行测试。你可以像这样运行测试 test/testdata/lsp/<name>.rb:
bazel test //test:test_LSPTests/testdata/lsp/<name>
测试“查找定义”和“查找所有引用”
LSP 测试可以访问 def 和 usage 断言,你可以使用它们来标注变量的
定义位置和引用位置:
a = 10
# ^ def: a
b = a + 10
# ^ usage: a
借助这些注释,测试将检查从加法处执行“查找定义”是否会跳转到
a = 10,并且从任一位置执行“查找所有引用”是否会同时返回定义和用法。
如果变量被重新定义,可以使用版本号进行注释:
a = 10
# ^ def: a 1
a = 20
# ^ def: a 2
b = a + 10
# ^ usage: a 2
usage 注释可以接受多个版本号,以 , 分隔。如果存在通过多条路径重新定义的变量,这将非常有用:
if some_condition
a = 10
# ^ def a 1
else
a = 'hello'
# ^ def: a 2
end
p a
# ^ usage: a 1,2
如果某个位置不应报告任何定义或用法,则使用魔法标签 (nothing):
a = 10
# ^ def: (nothing)
如果一个位置应报告多个定义(例如,在多个文件中打开的类或模块),则可以添加一个同名的第二个 def:
class Foo
# ^^^ def: foo
end
class Foo
# ^^^ def: foo
end
当标记对应于具有默认值的方法参数的定义时,需要标记多个定义:一个用于参数定义本身,另一个用于其默认值。默认值需要分配一个不同的版本号,并且也需要标记为 default-arg-value:
def foo(a: 1)
# ^ def: a 1
# ^ def: a 2 default-arg-value
p a
# ^ usage: a 1,2
end
这是由于默认值被翻译到 CFG 中:存在一个合成的条件分支,它选择要么从发送时传递的参数初始化变量,要么在不存在值时使用默认值。
在包规范(__package.rb)文件中,查找所有引用的方式有所不同。考虑以下情况:
class Foo < PackageSpec
import Bar
在此文件中对 Bar 调用“查找所有引用”将仅返回 Foo 包中对 Bar 的引用。LSP 测试
可以访问 import 和 importusage 断言,您可以使用它们来测试此功能。
class Foo < PackageSpec
import Bar
# ^^^ import: bar
class Foo::Baz
Bar.new
# ^^^ importusage: bar
end
借助这些注释,LSP 测试将检查在 import Bar 语句中对 Bar 执行“查找所有引用”是否返回了 Bar.new 用法。
请注意,import 断言与 def 断言不同,因为它实际上是 usage 断言的子类。
在这种情况下,与 import 对应的 def 是所导入包的 PackageSpec 声明。对 PackageSpec 声明执行“查找所有引用”
将返回该包的所有导入。
class Bar < PackageSpec
# ^^^ def: bar
import Bar
class Foo < PackageSpec
import Bar
# ^^^ import: bar
class Baz < PackageSpec
import Bar
# ^^^ import: bar
借助这些注释,LSP 测试将检查从 class Bar < PackageSpec 声明处对 Bar 执行“查找所有引用”时,是否返回该声明本身以及导入项。
测试“转到类型定义”
这与上述“查找定义”有些相似,但也略有不同,因为不存在“查找所有类型定义”的对应功能。
class A; end
# ^ type-def: some-label
aaa = A.new
# ^ type: some-label
type: some-label 断言表示“请在此处模拟跳转到类型定义,命名为 some-label”,而 type-def: some-label 断言表示“断言 some-label 的结果恰好是这些位置。”
这意味着如果类型定义可能返回多个位置,断言必须覆盖所有结果:
class A; end
# ^ type-def: AorB
class B; end
# ^ type-def: AorB
aaa = T.let(A.new, T.any(A, B))
# ^ type: AorB
如果某个位置不应报告任何定义或用法,则使用魔法标签 (nothing):
# typed: false
class A; end
aaa = A.new
# ^ def: (nothing)
测试悬停
LSP 测试还可以使用 hover 断言来验证悬停响应的内容:
a = 10
# ^ hover: Integer(10)
如果某个位置应报告空字符串,请使用特殊标签 (nothing):
a = 10
# ^ hover: (nothing)
使用 hover-line 断言来验证悬停响应中特定行的内容:
a = 10
# ^ hover-line: 1 Integer(10)
测试完成
LSP 测试还可以使用 completion
断言来验证补全响应的内容。
class A
def self.foo_1; end
def self.foo_2; end
foo
# ^ completion: foo_1, foo_2
end
^ 对应于光标的位置。因此,在上面的示例中,光标就像这样:foo│。如果 ^ 正好位于最后一个 o 的正下方,它将会是这样:fo|o。只有第一个 ^ 会被使用。如果你在断言中使用 ^^^,测试框架将在第一个光标的位置发送一个补全断言。
你还可以为补全结果的部分前缀编写测试:
class A
def self.foo_1; end
def self.foo_2; end
foo
# ^ completion: foo_1, ...
end
在部分补全结果列表的末尾添加 , ... 后缀,
测试框架将确保所列标识符匹配补全项的前缀。该前缀仍必须按顺序列出。
如果某个位置应报告零个补全项,请使用特殊消息
(nothing):
class A
def self.foo_1; end
def self.foo_2; end
zzz
# ^ completion: (nothing)
end
要为如果选择了某个补全项时将被插入到文档中的代码片段编写测试,你可以创建两个文件:
# -- test/testdata/lsp/completion/mytest.rb --
class A
def self.foo_1; end
end
A.foo_
# ^ apply-completion: [A] item: 0
The apply-completion 断言表示“确保文件 mytest.A.rbedited
包含将第 0 个补全项的补全片段插入文件后的结果。”
# -- test/testdata/lsp/completion/mytest.A.rbedited --
class A
def self.foo_1; end
end
A.foo_1${0}
# ^ apply-completion: [A] item: 0
如你所见,如果花哨的 ${...}(制表位占位符)在补全响应中被发送,它们会原样显示在输出中。
目前无法测试补全响应的这些部分:
- 补全类型
- 文档
- 详情
对于这些,你最好的选择是在 VS Code / 你首选的编辑器中手动测试,并验证你看到了你的更改。特别是对于文档,几乎所有代码路径都与悬停共享,因此你可以选择编写一个悬停测试。
测试工作区符号(符号搜索)
LSP 测试可以使用 symbol-search 断言来断言特定项出现在符号搜索(textDocument/workspaceSymbols 请求)中:
class Project::Foo
# ^^^ symbol-search: "Foo"
end
symbol-search 可以可选地指定该条目在搜索结果中应如何显示:
class Project::Foo
# ^^^ symbol-search: "Foo", name = "Foo", container = "Project"
end
在上述内容中,container 也可以是特殊字符串 "(nothing)",
以指示该条目没有容器。
symbol-search 还可以指定该条目在有序
搜索结果中的相对排名:
class Project::Foo
# ^^^ symbol-search: "Foo", rank = 1
end
测试“转到实现”
测试“转到实现”功能与测试“转到类型定义”的技术非常相似。
module A
# ^ find-implementation: A
extend T::Sig
extend T::Helpers
interface!
end
class B
#^^^^^^^ implementation: A
extend T::Sig
include A
# ^ find-implementation: A
end
有两种类型的断言:
find-implementation: <symbol>表示在此处发起“转到实现”请求。<symbol>标记我们要查找的符号名称。implementation: <symbol>标记对于给定的<symbol>的“转到实现”调用应返回的位置
如果请求返回多个位置,您应该用 implementation: <symbol> 标记所有位置
测试重命名常量
要为重命名常量编写测试,您需要至少创建两个文件:
# -- test/testdata/lsp/refactor/mytest.rb --
# typed: true
# frozen_string_literal: true
class Foo
class Foo
end
end
foo = Foo.new
# ^ apply-rename: [A] newName: Bar
这里的 apply-rename 断言表示“模拟用户从此光标位置开始重命名”。你需要添加一个反映更改结果应呈现样式的 .rbedited 文件。在这种情况下,该文件将如下所示:
# -- test/testdata/lsp/refactor/mytest.A.rbedited --
# typed: true
# frozen_string_literal: true
class Bar
class Foo
end
end
foo = Bar.new
# ^ apply-rename: [A] newName: Bar
你可以通过在测试中添加 invalid: true 来测试无效的 rename 是否未被应用,如下所示:
# -- test/testdata/lsp/refactor/mytest.rb --
# typed: true
# frozen_string_literal: true
class Foo
class Foo
end
end
foo = Foo.new
# ^ apply-rename: [A] newName: foo invalid:true
要测试特定的错误消息,请在测试中添加一个 expectedErrorMessage 参数:
# typed: true
# frozen_string_literal: true
require_relative './constant__class_definition.rb'
sig { params(foo: Foo::Foo).returns(Foo::Foo) }
def foo(foo); end
class Baz
# ^ apply-rename: [D] newName: Bar invalid: true expectedErrorMessage: Renaming constants defined in .rbi files is not supported; symbol Baz is defined at test/testdata/lsp/rename/constant__rbi_class_reference.rbi
end
你可以添加更多引用你正在重命名的常量的文件,只需确保
添加一个具有相同版本的匹配 .rbedited 文件。
测试增量类型检查
在 LSP 模式下,Sorbet 在 快速路径 或 慢速路径 上运行文件更新。它会检查更新前后的 文件结构,以确定该更改是否由快速路径覆盖。如果是, 它会执行进一步的处理以确定需要类型检查的文件集合。
LSP 测试可以在 <name>.<version>.rbupdate 文件中定义文件更新,这些文件包含更新发生后 <name>.rb
的内容。例如,文件 foo.1.rbupdate 包含 foo.rb 的更新内容。
如果测试通过使用带有 __ 后缀的前缀包含多个文件,则所有具有相同版本的 rbupdates 都将在
同一次更新中应用。例如,foo__bar.1.rbupdate 和 foo__baz.1.rbupdate 将同时应用
以更新 foo__bar.rb 和 foo__baz.rb。
在 *.rbupdate 文件中,你可以通过添加一行 # assert-slow-path: true 来断言慢速路径已运行。
你可以使用
#assert-fast-path: foo__bar.rb,foo__baz.rb 来断言快速路径在 foo__bar.rb 和 foo__baz.rb 上运行。
请注意,测试多文件更新(例如,
*__1.1.rbupdate + *__2.1.rbupdate)时的默认行为是包含创建并发送到 LSP 服务器的文件
更新中的所有文件。当测试使用
assert-fast-path 断言快速路径上是否对正确的文件进行了类型检查的更改时,
您可能还需要声明哪些文件不应
包含在文件编辑中,让 Sorbet 自行确定要类型检查的文件子集。
但是,无论文件是否包含在
更新集中,您可能都希望断言错误发生在文件内的特定点。为此,您可以在
rbupdate 文件中使用 # exclude-from-file-update: true。请注意,使用此功能时,在
rbupdate 中添加
exclude-from-file-update 断言的效果会使所有
error 断言与 LSP
服务器报告这些错误的位置相比偏移一行。为了规避此问题,您应该在
前一个文件中留一个空行,以便
exclude-from-file-update
断言替换该空行,而不是作为
完全新的一行插入到文件中。在部分 fast_path 测试中搜索 spacer 以查看
示例。
要创建对 RBI 文件的更新,请使用 .rbiupdate 而不是 .rbupdate,
除非您打算模拟将 RBI 文件转换为 RB 文件的效果。
测试 sorbet/hierarchyReferences
测试 sorbet/hierarchyReferences 有两种模式:
^ hierarchy-ref-set: <symbol>
从 <symbol> 指定的集合中的每个位置发起 sorbet/hierarchyReferences 请求时,
应包含集合中所有其他条目的 Locations 列表。
^ find-hierarchy-refs: <symbol>+^ hierarchy-ref: <symbol>
仅在 find-hierarchy-refs 指定的位置对每个位置发起 sorbet/hierarchyReferences 请求,应返回一个包含原始 find-hierarchy-refs 位置以及 hierarchy-ref 指定的其他位置的 Locations 列表。
优先使用 "set" 版本,因为它更简洁且测试了更多内容。
当存在有意设计的不对称性时,请使用 find-hierarchy-refs 版本。例如,在多重继承的情况下,两个父模块的层级结构可能互不相关,但它们共享的子模块的层级结构将同时包含这两个父模块。在这种情况下,只能使用 find-hierarchy-refs。
LSP 录制测试
可以录制一个 LSP 会话并将其用作测试。我们正试图摒弃这种测试形式,因为这些测试难以更新和理解。如果可能,请尽量将测试用例添加为常规的 LSP 测试。
添加到 test/lsp/ 的任何 <name> 文件夹都会成为一个测试。
该文件夹应包含一个名为 <folderName>.rec 的文件,其中包含录制的 LSP 会话。
- 以 "Read:" 开头的行将作为输入发送给 sorbet。
- 以 "Write:" 开头的行将作为来自 sorbet 的预期输出。
更新测试
当测试失败时,通常是因为捕获的输出中发生了无关紧要的变化,而不是代码中存在 bug。
要重新捕获跟踪,您可以运行
tools/scripts/update_exp_files.sh
您可能需要查看这些更改,并 git checkout 那些您认为实际上是代码中 bug 的有更改的文件,然后修复您的代码。
update_exp_files.sh 会更新 Sorbet 已知的每种快照文件类型。这可能会很慢,具体取决于需要重新编译和更新的内容。一些更快的命令:
# Only update the `*.exp` files in `test/testdata`
tools/scripts/update_testdata_exp.sh
# Only update the `*.exp` files in `test/testdata/cfg`
tools/scripts/update_testdata_exp.sh test/testdata/cfg
# Only update a single exp file's test:
tools/scripts/update_testdata_exp.sh test/testdata/cfg/next.rb
# Only update the `*.out` files in `test/cli`
bazel test //test/cli:update
调试
通常,
- 调试 sorbet 的常规构建?
lldb bazel-bin/main/sorbet -- <args> ...- (考虑使用
--config=static-libs以获得更好的调试符号) - 如果在 macOS 上看到奇怪的 Python 错误,请尝试
PATH=/usr/bin lldb。
- 调试现有的 Sorbet 进程(即 LSP)
- 使用
--wait-for-dbg标志启动 Sorbet lldb -p <pid>- 设置断点,然后
continue
- 使用
此外,养成先添加一个
ENFORCE(断言)来捕获该 bug,然后再实际修复
bug 的习惯是很好的。当有清晰的错误消息说明你违反了哪个
不变量时,修复 bug 会容易得多。在发布构建中,ENFORCE 是免费的。
编写文档
Sorbet 文档网站的源代码位于
website/ 文件夹中。具体来说,文档位于
website/docs/,全部使用 Markdown 编写,并使用 Docusaurus 构建。
^ 请参阅此处了解如何在本地处理文档站点。
编辑器与环境
Bazel
Bazel 支持保留之前构建结果的持久缓存,以便
对相同输入文件的重新构建速度更快。要启用此功能,请运行此
脚本以创建 ./.bazelrc.local 和缓存文件夹:
tools/create_local_bazelrc.sh
Shell
许多构建命令都非常长。你可以考虑使用自己选择的 shell 别名来缩短常用的命令:
# mnemonic: 's' for sorbet
alias sb="bazel build //main:sorbet --config=dbg"
alias st="bazel test //... --config=dbg --test_output=errors"
格式化文件
我们确保 C++ 文件使用 clang-format 进行格式化,并且 Bazel BUILD
文件使用 buildifier 进行格式化。为了避免这些工具不同版本之间
的不一致,我们有通过 bazel 下载并运行这些工具的脚本:
tools/scripts/format_cxx.sh
tools/scripts/format_build_files.sh
如果存在任何未格式化的文件,CI 将会失败,因此你可能希望使用以下选项之一设置文件自动格式化:
- 设置一个 pre-commit / pre-push 钩子来运行这些脚本。
- 设置你的编辑器来运行这些脚本。详见下文。
C++ 编辑器配置
clang 工具套件在编辑器工具方面有着相当出色的支持:你可以使用 Clang 的 Compilation Database 格式构建一个 compile_commands.json。
许多基于 clang 的工具会消费此文件,以在编辑器集成中提供语言感知功能,例如。
使用 bazel 为 Sorbet 构建 compile_commands.json 文件:
tools/scripts/build_compilation_db.sh
这会构建一个 ./compile_commands.json 文件(该文件已被 git 忽略)。此文件
将某些路径硬编码到 Bazel 沙箱中。这些文件可能会过时,
尤其是在它们由 Bazel genrule 生成时。具体来说,
./compile_commands.json 引用了 Bazel 的 opt 配置中的文件(例如,
最近使用 -c opt / --compilation_mode=opt 构建的内容)。如果你看到
过时的错误,请考虑运行类似 ./bazel build //main:sorbet -c opt 的命令。
我们鼓励你尝试使用 compile_commands.json 数据库的各种基于 clang 的工具。一些建议:
-
rtags -- 支持 Clang 的跳转定义 / 查找引用 / 等等。
brew install rtags # Have the rtags daemon be automatically launched by macOS on demand brew services start rtags # cd into sorbet # ensure that ./compile_commands.json exists # Tell rtags to index sorbet using our compile_commands.json file rc -J .
大多数文本编辑器都有 rtags 编辑器插件。
-
clangd -- 基于 Clang 的语言服务器实现
clangd支持比rtags更多的功能(具体而言,报告 Diagnostics),但由于它不像 rtags 那样预先索引所有代码, 有时可能会稍慢一些。成功编译 Sorbet 后,将编辑器指向位于
bazel-sorbet/external/llvm_toolchain_15_0_7/bin/clangd中的clangd可执行文件。 -
clang-format -- 基于 Clang 的源代码格式化工具
我们在 Bazel 中构建
clang-format,以确保每个人使用相同的 版本。以下是如何从 Bazel 获取clang-format并在 编辑器中使用它的方法:# Build clang-format with bazel ./bazel build //tools:clang-format # Once bazel runs again, this symlink to clang-format will go away. # We need to copy it out of bazel so our editor can use it: mkdir -p "$HOME/bin" cp bazel-bin/tools/clang-format $HOME/bin # (Be sure that $HOME/bin is on your PATH, or use a path that is)
将 clang-format 添加到你的路径中,你应该能够找到一个编辑器
插件,它使用它来在保存时格式化你的代码。
注意:我们的格式化脚本向 `clang-format` 传递了一些额外的选项。
配置你的编辑器,将这些选项传递给 `clang-format`:
```shell
-style=file -assume-filename=<CURRENT_FILE>
```
-
clang-tidy -- 基于 Clang 的静态分析 / 代码检查
我们通过 Bazel aspect 在 CI 中运行
clang-tidy。要在本地运行:# Run on all targets ./bazel build --keep_going --config=clang-tidy --config=dbg //... # Run on a single target (note: only runs on files *directly* in that target) ./bazel build --config=clang-tidy --config=dbg //main/lsp:lsp
这些检查及其配置位于仓库根目录的 .clang-tidy 中。
`clang-tidy` 支持与 `clangd` 相同的 `compilation_commands.json` 数据库,但不幸的是,它似乎不支持 `directory`
参数。我们有一个包装脚本,你可以将编辑器指向该脚本,它会 `cd`
然后运行由我们的工具链构建的 `clang-tidy`:
```vim
if filereadable("./compile_commands.json")
" I set these to use clangd via nvim-lsp, but you could have clangd here.
let g:ale_linters.c = []
let g:ale_linters.cpp = []
if filereadable(".clang-tidy") && filereadable("tools/scripts/clang-tidy")
let g:ale_linters.c += ['clangtidy']
let g:ale_linters.cpp += ['clangtidy']
let g:ale_cpp_clangtidy_executable = 'tools/scripts/clang-tidy'
let g:ale_cpp_clangtidy_extra_options = '--use-color=0'
endif
endif
```
与 clangd 一样,这要求你最近构建过 //main:sorbet 或
包含你希望检查的文件的某个目标。
-
CLion -- JetBrains C/C++ IDE
可以让 CLion 识别
compile_commands.json数据库。 它会取代你整个文本编辑工作流(功能完整的 IDE)。 -
vscode-clangd -- VS Code 的 Clangd 扩展
此扩展将 clangd(见上文)与 VS Code 集成。它还会 在你每次保存时运行
clang-format。注意:Microsoft 的 C/C++ 扩展 与 Sorbet 的compile_commands.json无法正常配合使用。此仓库的设置会自动配置 vscode-clangd 以 在
bazel-sorbet目录中运行 clangd 可执行文件。请注意, 你需要先编译一次 Sorbet 才能使其正常工作。clangd 基于
compile_commands.json运行,因此请确保你运行了./tools/scripts/build_compilation_db.sh脚本。
以下是一些示例配置:
测试/testdata 的编辑器配置
Sorbet 开发者希望将其编辑器配置为以与常规 Sorbet 用户略有不同的方式运行 Sorbet。Sorbet 开发者配置的两个主要目标:
- 使用
bazel-bin/main/sorbet以便在重新构建时于编辑器中查看 Sorbet 的更改 - 使
test/testdata/及其他文件通过类型检查
由于 Sorbet 开发者数量极少,每种配置都是定制的。
@jez 的配置
-
在
$PATH的某处创建一个指向/path/to/sorbet/bazel-bin/main/sorbet的sorbet符号链接。 -
配置 LSP 以如下方式调用 Sorbet LSP 服务器:
-
如果当前文件夹中存在
Gemfile,则使用srb tc。否则, 使用sorbet(来自$PATH配置,见上文)。 -
如果仓库中存在
sorbet/config文件,则不带额外参数调用。 否则,使用以下参数调用:-e 0 /Users/jez/.local/share/sorbet/.empty --debug-log-file=/tmp/sorbet-nvim.log
-
这意味着在 test/testdata/ 中打开任何 Ruby 文件时,
Sorbet 应自动启动,并仅对该文件进行独立的类型检查,
而不是尝试将 test/testdata/ 中的所有文件作为一个项目
进行类型检查。
others
如果你觉得有用,请随时添加你的!