Ruby 3 RBS 类型检查

Dai*_*iro 4 ruby rbs

不了解如何编写和设置 ruby​​ 3 RBS 类型检查。

假设我有一个类似的文件:

### file name: my_project/book.rb
class Book
  def initialize(pages)
    @pages = pages
  end
  
  def pages
    @pages
  end
end

first_book = Book.new(100)
puts first_book.pages
Run Code Online (Sandbox Code Playgroud)

问题:.rbs对于我的文件示例,文件应该是什么样的?我应该运行什么 CLI 命令来测试它?例如$rbs my_project *或类似的东西?

Jör*_*tag 11

不了解如何编写和设置 ruby​​ 3 RBS 类型检查。

你在这里混淆了三个完全不同的东西:

  1. 一个类型语言
  2. 一个类型系统
  3. 一个类型检查

RBS 是#1,一种类型语言。这正是它听起来的样子:一种用于写下Types语言

它说,没有什么这些类型的意思(这将是一个类型系统),并没有提到一个程序是否被键入精心对于一个类型的系统(这将是一个类型检查)。

RBS 是基于观察到社区中已经存在多个类型检查器而开发的。例如,几乎每个 Ruby IDE 都包含一个类型检查器,以便能够提供更好的代码完成和“红色波浪线”。有冰糕。有陡峭。还有其他几个。

Ruby 社区中已经存在多种不兼容的类型语言。Ruby 核心库和标准库 RDoc 注释有时使用非正式的注释。YARD 有类型注释,但没有类型语言。(他们只给出了一个类型语言可能是什么样子的例子。)有些人使用示例类型语言编写 YARD 类型注释,所以使用自产的或修改过的。然后是 Sorbet 使用的 RBI。

RBS 背后的想法是提供一种类型语言作为 Ruby 语言的一部分,并带有一个用于查询该语言的 API。这将一方面允许现有类型检查器之间的互操作性,另一方面允许更多的竞争。(目前,有一个锁定效应:每个类型检查器使用不同的类型语言,因此如果不使用不同的类型语言从头开始重写类型签名,就无法轻松交换它们。如果它们使用公共类型语言,您可以轻松地更换它们并为您的用例选择最好的一个。)

RBS 项目的后半部分是为整个 Ruby 核心库和标准库提供一套完整的 RBS 签名。这是当前事务的另一个问题:每个类型检查器都必须一遍又一遍地执行此操作,他们都必须从头开始使用自己的类型语言为 Ruby 核心和标准库编写类型签名。对于 RBS,Ruby 核心和标准库将有一组标准的类型签名。

此外,为 Rails 等项目编写签名变得更加容易,因为您只需编写组签名,并且您知道它适用于每个类型检查器、每个 IDE、每个 linter、每个静态分析器等。

问题:.rbs对于我的文件示例,文件应该是什么样的?

这取决于:你想说什么?

class Book[T]
  @pages: T
  def initialize: (pages: T) -> void
  def pages: () -> T
end
Run Code Online (Sandbox Code Playgroud)

是一种可能的类型定义。

class Book[T]
  attr_reader pages: T
  def initialize: (pages: T) -> void
end
Run Code Online (Sandbox Code Playgroud)

可能比第一个更好地捕捉您的意图。(因为我不知道你的意图,但只能看到你的代码,我不知道哪个更好。)

class Book
  attr_reader pages: 100
  def initialize: (pages: 100) -> void
end
Run Code Online (Sandbox Code Playgroud)

是另一种可能的类型定义。这是最严格的类型签名,它仍然允许您的示例代码运行。

class Book
  attr_reader pages: Numeric
  def initialize: (pages: Numeric) -> void
end
Run Code Online (Sandbox Code Playgroud)

也可以,原样

class Book
  attr_reader pages: Integer
  def initialize: (pages: Integer) -> void
end
Run Code Online (Sandbox Code Playgroud)

当然,这也总是有效的,尽管没有用,因为它没有告诉我们任何我们还不知道的事情:

class Book
  attr_reader pages: untyped
  def initialize: (pages: untyped) -> untyped
end
Run Code Online (Sandbox Code Playgroud)

从技术上讲,您代码中的两种方法都采用可选块,而我们在此处不允许这样做。为了满足你的代码的语义准确,签名也许应该是这样的:

class Book[T]
  @pages: T
  def initialize: (pages: T) ?{ (*args: void) -> void } -> void
  def pages: () ?{ (*args: void) -> void } -> T
end
Run Code Online (Sandbox Code Playgroud)

最后一个是与您的代码当前所做和允许的最匹配的一个。这是否也准确地捕获了您希望代码执行和允许的操作,这是一个完全不同的问题,只有您才能回答。

我应该运行什么 CLI 命令来测试它?

目前还不清楚“测试”是什么意思。如果您的意思是“类型检查”,那么正如我之前所说:RBS不是类型检查器,它只是一种用于编写类型的语言,它不知道也不关心您这些类型什么。

然而,RBS确实有一个测试模式,但它做一些不同的事情:它可以查看您正在运行的代码并查看您的代码是否在运行时违反了任何类型约束。但是,这不是静态类型检查。

例如,如果您有此签名:

class Foo
  def bar:  () -> Integer
  def quux: () -> Integer
end
Run Code Online (Sandbox Code Playgroud)

和这个代码:

class Foo
  def bar;  23           end
  def quux; 'fourty-two' end
end

foo = Foo.new
foo.bar
Run Code Online (Sandbox Code Playgroud)

不会抱怨,因为它从未观察到Foo#quux在运行时违反类型约束。

如果你有这个定义Foo#quux

def quux; if rand < 0.5 then 42 else 'fourty-two' end end
Run Code Online (Sandbox Code Playgroud)

然后它有时会抱怨,有时不会,只要你真的打电话quux到某个地方。

因此,您是否发现任何问题取决于您的测试覆盖率。如果您的方法在使用特定参数组合调用时返回错误类型,但您从未在测试中使用此特定参数组合调用它,那么您将永远不会注意到。

请注意,这与其说是测试代码的正确性,不如说是测试类型签名的正确性。

例如$rbs my_project *或类似的东西?

RBS 附带的工具主要设计为库,因为预计它们将集成到文档工具(以在文档中显示类型签名)、测试工具(以测试您的类型签名)、IDE(用于代码完成) ),或在类型检查器中

例如,我上面提到的测试工具打算这样使用:

RBS_TEST_TARGET='Foo::*' bundle exec ruby -r rbs/test/setup test/foo_test.rb
Run Code Online (Sandbox Code Playgroud)

什么rbs/test/setup都会做,很愚蠢地只问了Foo.constants.grep(Module),然后为每个这些模块做mod.instance_methods(false)和字面与包装,检查类型在方法的入口和出口,并调用原来的方法替换每个方法。(使用与 ActiveSupport 相同的技术alias_method_chain。)

如您所见,这对命令行不是很友好,它旨在作为库集成到测试工具中。

目前几个可用的命令行工具:

  • rbs ast 以 JSON 格式打印当前环境的 AST
  • rbs list 打印出当前环境中的类型列表
  • rbs ancestors 打印出模块的祖先
  • rbs methods 打印出模块的方法
  • rbs method 打印出一个方法
  • rbs validate 验证 RBS 文件的语法并进行一些基本的语义完整性检查
  • rbs constant 执行不断的查找
  • rbs paths 打印出 RBS 查找签名文件的路径
  • rbs prototype可以生成一个骨架类型签名来帮助您入门,它可以从您的代码或现有的 RBI 类型签名中执行此操作。(RBI 是 Sorbet 类型检查器使用的类型语言。)
  • rbs vendor 供应商签名文件放入项目目录
  • rbs parse 解析 RBS 文件并打印语法错误
  • rbs test 使用注入的上述测试钩子运行您的测试

但是,请注意,此 CLI并非旨在作为 RBS 的主要接口。该README明确地说:

gem 附带rbs命令行工具以演示它可以做什么并帮助开发 RBS。

CLI 旨在作为如何使用 API的示例,而不是作为 API 的主要入口点。


mig*_*ano 6

我尝试使用 ruby​​ 3.0.0 制作一个简单的示例来测试使用 rbs 语法的静态分析。

1.Ruby文件

# book.rb
class Book
  def initialize(pages)
    @pages = pages
  end

  def pages
    @pages
  end
end

first_book = Book.new(100)
puts first_book.pages
Run Code Online (Sandbox Code Playgroud)

2.生成rbs文件

typeprof book.rb > book.rbs
Run Code Online (Sandbox Code Playgroud)

3. 使用陡峭的CLI 作为类型检查器。

首先,我们需要这个配置文件:

# Steepfile
target :lib do
  signature "."
  check "book.rb"
end
Run Code Online (Sandbox Code Playgroud)

并运行:

steep check
Run Code Online (Sandbox Code Playgroud)

空消息意味着静态类型检查器中没有错误。要重现静态类型错误,我们可以使用字符串调用初始化程序

typeprof book.rb > book.rbs
Run Code Online (Sandbox Code Playgroud)

steep check抛出以下错误

book.rb:11:22: ArgumentTypeMismatch: receiver=singleton(::Book), expected=::Integer, actual=::String ("100")
Run Code Online (Sandbox Code Playgroud)

通过这个例子,我们可以证明我们可以使用 rbs 类型定义来验证我们的 ruby​​ 程序。