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

问题描述 投票:0回答:3

我正在一个实验性开源项目中尝试 Sorbet (ruby_crystal_codemod)。我不知道如何让类型检查与嵌套测试项目中的某些 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.

这里是 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文件是否有问题?

ruby types typechecking sorbet
3个回答
0
投票

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


0
投票

生成的打点文件没有任何问题。

RSpec 非常依赖 DSL。当您调用像

RSpec.describe
这样接受块的方法时,
RSpec
将在具有特定绑定的特定范围内执行该块,从而允许您调用 RSpec 测试 DSL。

为了让 Sorbet 获得 RSpec 方法的知识,它需要了解这种行为。您可以通过多种方式向 Sorbet 提示将执行方法接收的代码块的绑定。您可以在 Sorbet 文档 上阅读相关内容。

如果您查看您提到的 RBI 文件,它不包含此信息。 gem 的自动打点生成器通常仅限于确定 gem 中存在哪些常量(类、模块)和哪些方法。这不包括这些方法或其参数的签名。

如果没有关于某个块的绑定的提示,Sorbet 会假定该块已绑定到该块的外部上下文。例如,如果您在 Ruby 的顶层使用

RSpec.describe
,则 Sorbet 将为传递给
describe
的块假定上下文。由于全局 Ruby 范围内没有
eq
的定义,类型检查将会失败。

为了解决这个问题,您可以做很多事情,涉及不同程度的努力和奖励。

  1. 不要在测试中使用Sorbet。有些人会认为测试不需要键入。在不得不在非常大的单体应用程序中重构大量代码之后,我强烈不同意这一点,但事实是这是最轻松的解决方案。只需将
    # typed: ignore
    放在所有规范文件的开头即可继续。您将错过测试套件中静态类型的所有好处。
  2. 创建一个 shim 暗示 Sorbet 传递给
    RSpec.describe
    的块应该绑定到
    T.untyped
    我过去已经成功地使用了这个解决方案。您不会获得 Sorbet 的所有好处,特别是在 RSpec 方法上,但至少您在调用自己的代码时会得到类型检查。请参阅下面的示例,了解其外观。这将允许您在规范文件上使用
    # typed: true
  3. 创建一个垫片,将 RSpec 中的每个方法绑定到正确的类。这将花费很长时间,并且无疑是一项巨大的努力,但如果您设法做到这一点,您将获得使用 Sorbet 的几乎所有好处R规格。您可以与社区分享。这可能还需要为 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 类中,您可能也需要对其他方法重复此操作,直到您完全说服类型检查器让您独自一人。

有了这个,您应该能够在规范文件中使用

# typed: true
。它并不完美,实际上它只不过是一个补丁,但它至少会给你一些非常基本的类型检查功能。


0
投票

这是一个很好的解决方案:

RSpec.describe(MyModel) do
  T.bind(self, T.untyped)
  # add your tests...
end

这允许在

it
块等中进行自动补全和类型检查的好处,并使其忽略不在 sorbet 知识范围内的“it”和“describe”块。

© www.soinside.com 2019 - 2024. All rights reserved.