ruby 为什么“srb tc”没有为我的RSpec测试找到“expect”和“eq”方法?

w7t8yxp5  于 2023-06-29  发布在  Ruby
关注(0)|答案(2)|浏览(116)

我正在一个实验性的开源项目(ruby_crystal_codemod)中试用Sorbet。我不知道如何让类型检查在嵌套测试项目中的一些RSpec测试中工作。当我运行srb tc时,我看到一些类型检查错误,如下所示:

spec/src/example_class_annotated_spec.rb:6: Method it does not exist on T.class_of(<root>) https://srb.help/7003
     6 |  it 'should add @foo and @bar' do
     7 |    instance = ExampleClass.new(2, 3, 4)
     8 |    expect(instance.add).to eq 5
     9 |  end

spec/src/example_class_annotated_spec.rb:8: Method expect does not exist on T.class_of(<root>) https://srb.help/7003
     8 |    expect(instance.add).to eq 5
            ^^^^^^^^^^^^^^^^^^^^
    https://github.com/sorbet/sorbet/tree/67cd17f5168252fdec1ad04839b31fdda8bc6155/rbi/core/kernel.rbi#L2662: Did you mean: Kernel#exec?
    2662 |  def exec(*args); end
            ^^^^^^^^^^^^^^^

spec/src/example_class_annotated_spec.rb:8: Method eq does not exist on T.class_of(<root>) https://srb.help/7003
     8 |    expect(instance.add).to eq 5
                                    ^^^^

# etc.

这是source directory for the nested project on GitHub

您应该能够运行以下命令来重现类型错误:

cd /tmp
git clone https://github.com/DocSpring/ruby_crystal_codemod.git
cd ruby_crystal_codemod
git checkout sorbet-rspec-type-checking-error
cd spec/fixtures/rspec_project/
bundle install
bundle exec srb tc

您应该看到这些类型错误:

spec/src/example_class_annotated_spec.rb:6: Method it does not exist on T.class_of(<root>) https://srb.help/7003
     6 |  it 'should add @foo and @bar' do
     7 |    instance = ExampleClass.new(2, 3, 4)
     8 |    expect(instance.add).to eq 5
     9 |  end

# etc.

spec/fixtures/rspec_project/sorbet/rbi/gems/rspec-core.rbi等处的RBI文件是否有问题?

rsaldnfx

rsaldnfx1#

也许使用tapioca生成类型定义会对https://github.com/Shopify/tapioca有所帮助

7nbnzgx9

7nbnzgx92#

生成的RBI文件没有任何问题。
RSpec非常依赖DSL。当您调用像RSpec.describe这样的方法时,它接受一个块,RSpec将在特定范围内使用特定绑定执行该块,允许您调用RSpec测试DSL。
对于Sorbet来说,要获得RSpec方法的知识,它需要知道这种行为。您可以通过多种方式提示Sorbet有关方法接收的代码块将在其中执行的绑定。您可以在Sorbet文档中阅读它。
如果你看一下你提到的RBI文件,它不包括这些信息。gem的自动RBI生成器通常仅限于确定gem中存在哪些常量(类,模块)和哪些方法。这不包括这些方法或其参数的签名。
如果没有关于某个块的绑定的提示,则Sorbet假定该块绑定到该块的外部上下文。例如,如果您在Ruby的顶层使用RSpec.describe,这就是Sorbet将为传递给describe的块假定的上下文。由于在全局Ruby作用域中没有eq的定义,因此类型检查将失败。
为了解决这个问题,你可以做很多事情,包括不同程度的努力和回报。
1.**不要在你的测试中使用果汁冰糕。**有些人会争辩说测试不需要打字。在不得不在非常大的单体应用程序中重构大量代码之后,我强烈反对这种做法,但事实是这是最轻松的解决方案。只需将# typed: ignore放在所有规范文件的开头,然后继续。您将错过测试套件中静态类型的所有好处。
1.**创建一个shim,提示Sorbet传递给RSpec.describe的块应该绑定到T.untyped。**我过去成功地使用了这个解决方案。您不会得到Sorbet的所有好处,特别是在RSpec方法上,但至少在调用自己的代码时可以进行类型检查。下面是一个例子,说明这是什么样子。这将允许您在spec文件上使用# typed: true
1.**创建一个shim,将RSpec中的每个方法绑定到正确的类。**这将花费很长时间,无疑是一个非常大的努力,但如果你设法做到这一点,你将获得几乎所有使用RSpec的Sorbet的好处。你可以share it with the community。这可能还需要为Tapioca编译器编写代码,为使用let(:something)定义的内容生成适当的类型。
我不相信1和3是一个很大的工作,所以现在我将详细说明如何进行第二个选项。
sorbet/rbi/shims/rspec.rbi中为rspec创建一个shim文件,如下所示:

# typed: strict

module RSpec
end

在这个文件中,我们将添加必要的签名,以不假设任何关于传递给RSpec方法的块的绑定。例如,让我们从describe开始。如果你去自动生成的RBI,你会看到它有这样的声明:

def self.describe(*args, &example_group_block); end

让我们将其复制到我们自己的RBI文件中,并添加我们自己的签名。

# typed: strict

module RSpec
  sig do
    params(
      args: T.untyped,
      example_group_block: T.proc.bind(T.untyped).void
    ).void
  end
  def self.describe(*args, &example_group_block); end
end

我们现在已经告诉Sorbet,你传递给describe的块绑定到T.untyped。此类型充当了退出窗口,允许任何方法调用,因此Sorbet不会抱怨其中缺少方法。您可能需要对其他方法重复此操作,甚至在不同的RSpec类中,直到您完全说服类型检查器不再打扰您。
这样你就可以在spec文件中使用# typed: true了。它并不完美,实际上它只不过是一个补丁,但它至少会授予您一些非常基本的类型检查功能。

相关问题