Type definitions were introduced in Ruby 3.0. Ruby 3 \ .0 \ .0 released
I wanted to check the procedure when actually adding to the existing code, so the library (Gem) I created for studying just before seemed to be small and easy to introduce, so I added a type definition to this So, I checked the flow until the Gem user actually uses the type definition. kuredev/simple_ping: Simpe Ping Client for Ruby
Generally, referring to the flow of the following article, I will summarize what I confirmed and investigated in each step. I tried to write a static type definition of Ruby 3 \ .0 in a library like TypeScript -Narazaka :: Blog
First of all, we need to create an RBS file to add the type definition. Since it is difficult to make from scratch by hand, we will first consider creating a rough one automatically. There are three main ways to create RBS automatically,
If possible, I wanted the argument/return type information to be created automatically, so I first tried using TypeProf.
Characteristics of each method to generate RBS from Ruby -pockestrap
[Ruby 3 \ .0] Memo -memo \ .log to try various RBS generation methods
However, when I actually tried to output the type definition with typeprof, the following error occurred.
% typeprof lib/simple_ping/client.rb
# Analysis Error
A constant `MonitorMixin' is used but not defined in RBS
The message says that there is no MonitorMixin
module definition in RBS.
I'm using the logger library in the library, but it seems that Logger :: LogDevice
in it is using MonitorMixin
.
Typedefing a simple code like the one below still gave the same error, so it seemed to be reproducible.
I haven't gotten too deep into it, but many of the Ruby standard libraries already provide RBS type definition information, but I wonder if typeprof also failed to interpret the type because there is no definition for MonitorMixin
yet. I think.
tmp.rb
require "logger"
class Kure
def self.run
logger = Logger.new
logger.info "hoge"
end
end
It seemed that it could not be solved immediately, so this time I decided to prepare RBS with rbs prototype runtime
.
I also tried rbs prototype rb
, but I chose this because the result was simpler with rbs prototype runtime
due to the format and the presence or absence of comments.
If you want to create RBS with rbs prototype runtime
, you need the code to execute the program, so prepare the following code at the top of the repository.
sample_run.rb
require_relative "./lib/simple_ping"
ping_client = SimplePing::Client.new(src_ip_addr: "172.31.7.56")
puts ping_client.exec(dest_ip_addr: "8.8.8.8")
Execute as follows.
% sudo rbs prototype runtime -R sample_run.rb "SimplePing::*"
class SimplePing::Client
public
def exec: (dest_ip_addr: untyped, ?data: untyped) -> untyped
private
def initialize: (src_ip_addr: untyped, ?log_level: untyped) -> untyped
def logger: () -> untyped
def socket: () -> untyped
end
SimplePing::Client::TIMEOUT_TIME: Integer
class SimplePing::ICMP
public
def data: () -> untyped
def data=: (untyped) -> untyped
def id: () -> untyped
def id=: (untyped) -> untyped
def is_type_destination_unreachable?: () -> untyped
def is_type_echo?: () -> untyped
def is_type_echo_reply?: () -> untyped
def is_type_redirect?: () -> untyped
def seq_number: () -> untyped
def seq_number=: (untyped) -> untyped
def successful_reply?: (untyped icmp) -> untyped
def to_trans_data: () -> untyped
def type: () -> untyped
def type=: (untyped) -> untyped
private
def carry_up: (untyped num) -> untyped
def checksum: () -> untyped
def gen_data: () -> untyped
def gen_id: () -> untyped
def gen_seq_number: () -> untyped
def initialize: (type: untyped, ?code: untyped, ?id: untyped, ?seq_number: untyped, ?data: untyped) -> untyped
end
SimplePing::ICMP::TYPE_ICMP_DESTINATION_UNREACHABLE: Integer
SimplePing::ICMP::TYPE_ICMP_ECHO_REPLY: Integer
SimplePing::ICMP::TYPE_ICMP_ECHO_REQUEST: Integer
SimplePing::ICMP::TYPE_ICMP_REDIRECT: Integer
class SimplePing::RecvMessage
public
def code: () -> untyped
def data: () -> untyped
def id: () -> untyped
def seq_number: () -> untyped
def to_icmp: () -> untyped
def type: () -> untyped
private
def initialize: (untyped mesg) -> untyped
end
Save the above result as an RBS file in the sig
directory, and use steep check
to check if there is a problem with the type information.
steep preparation
% gem install steep
% mkdir sig
% vim sig/simple_ping.rbs #Paste the above result
% steep init
% vim Steepfile #Described below
target :lib do
signature "sig"
check "lib"
check "./"
end
Run
% steep check
sig/simple_ping.rbs:1:0...13:3 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:17:0...61:3 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:71:0...89:3 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:15:0...15:41 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:63:0...63:60 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:65:0...65:47 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:67:0...67:49 UnknownTypeNameError: name=::SimplePing
sig/simple_ping.rbs:69:0...69:45 UnknownTypeNameError: name=::SimplePing
Since it is the type definition information created automatically, I would like it to succeed, but it has failed. I was angry that the :: SimplePing
was missing (defined in the code but not in the RBS file). Since an error occurred each time I made corrections including this, I will manually correct the RBS file in some respects. (There may be some errors that can be avoided by the argument of the command when generating rbs)
This time I tried to fix it as follows.
――If you do not write the top three lines, an error will occur, so I wrote them.
--The bottom four lines are the constructors and methods of the SimplePing :: Clinet
class. The information generated by the rbs command basically has an argument/return type of untyped, but since I wanted to write the argument type etc. for this class that the user of this Gem uses directly, from untyped I modified it and modified it to the specification type.
simple_ping.rbs
+ module SimplePing
+ end
+ SimplePing::VERSION: String
- def initialize: (src_ip_addr: untyped, ?log_level: untyped) -> untyped
+ def initialize: (src_ip_addr: String, ?log_level: Integer) -> void
- def exec: (dest_ip_addr: untyped, ?data: untyped) -> untyped
+ def exec: (dest_ip_addr: String, ?data: String) -> bool
Check the steep again and confirm that it works without any problem.
Run
% steep check
#OK if nothing is output
Try writing the wrong code on purpose to see if type checking works. Let's modify the execution file prepared above as follows.
sample_run.rb
require_relative "./lib/simple_ping"
ping_client = SimplePing::Client.new(src_ip_addr: 100) #Originally"IP Address"The part specified by is Integer
puts ping_client.exec(dest_ip_addr: 100) #Originally"IP Address"The part specified by is Integer
When I did a steep check, he pointed out that it was incorrect according to the type definition information.
% steep check
sample_run.rb:3:50: IncompatibleAssignment: lhs_type=::String, rhs_type=::Integer (100)
::Integer <: ::String
::Numeric <: ::String
::Object <: ::String
::BasicObject <: ::String
==> ::BasicObject <: ::String does not hold
sample_run.rb:4:36: IncompatibleAssignment: lhs_type=::String, rhs_type=::Integer (100)
::Integer <: ::String
::Numeric <: ::String
::Object <: ::String
::BasicObject <: ::String
==> ::BasicObject <: ::String does not hold
By the way, if you put the Steep plugin in VS Code, it will point out even while writing the code. It's convenient!
Put the created definition information and release it.
I tried writing a static type definition of Ruby 3 \ .0 in a library like TypeScript -Narazaka :: Blog As mentioned in the article, put the definition file in the sig
directory. If you use it, it will be read automatically when you use it, so put the created RBS file and release it.
% git add sig/simple_ping.rbs
% git commit -m "xxx"
% [Release work below...]
Try installing and using the released Gem in another environment. If you want to refer to the type definition of the library put in Bundler, it seems that you need to execute Steep put in Bundler, so put Steep in Bundler as well.
% bundle init
% vim Gemfile #Add the following. Version should be read as appropriate
gem "simple_ping", "0.1.1"
gem "steep"
% bundle install --path vendor/bundle
After installation, prepare for steep.
% bundle exec steep init
% vim Steepfile
target :lib do
signature "sig"
check "./"
library "simple_ping"
end
If you write like library" simple_ping "
, it seems that RBS under sig installed by Bundler will be read.
In this environment, I dare to write the code using Gem in the wrong form as before.
sample_run.rb
require "simple_ping"
SimplePing::Client.new(src_ip_addr: 1) #Specify with Integer where it should be specified with a character string
Try running steep check
.
He pointed out an error in the type information!
I will omit the image, but it will warn you properly on VS Code.
% bundle exec steep check
sample_run.rb:3:36: IncompatibleAssignment: lhs_type=::String, rhs_type=::Integer (1)
::Integer <: ::String
::Numeric <: ::String
::Object <: ::String
::BasicObject <: ::String
==> ::BasicObject <: ::String does not hold
That's all, thank you for your hard work.
[Ruby] I made a simple Ping client -Qiita
Recommended Posts