The other day, Ruby 3.0.0-preview2 was released on December 8th. https://www.ruby-lang.org/ja/news/2020/12/08/ruby-3-0-0-preview2-released/ The word "static analysis" is in it! In this article, I will immediately use "RBS", "TypeProf", and "Steep" related to static analysis of Ruby. I will describe how to use it without touching on the detailed theory and internal implementation.
If you have not installed it, please install it.
shell
$ brew upgrade rbenv ruby-build
shell
$ rbenv install 3.0.0-preview2
# 3.0.0-Compromise with preview1
$ rbenv install 3.0.0-preview1
$ rbenv local 3.0.0-preview1
root/
- lib/
- user.rb
- sig/
- user.rbs
- .ruby-version
- main.rb
- Gemfile
- Gemfile.lock
- Steepfile
Then install TypeProf and Steep used for type checking.
shell
#Creating a Gemfile template
bundle init
Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
# gem "rails"
gem "rbs"
gem "typeprof"
gem "steep"
shell
#Install rbs, typeprof and steep
bundle install
lib/user.rb
class User
def initialize(name:, age:)
@name, @age = name, age
end
attr_reader :name, :age
end
main.rb
require "./lib/user"
def main
user = User.new(name: "John", age: 20)
puts user.name
end
if __FILE__ == $0
main
end
shell
$ ruby main.rb
John
RBS
RBS is a language for writing types of Ruby programs. Static analysis tools such as TypeProf can analyze Ruby programs more accurately by using RBS. RBS defines the types of classes and modules in Ruby programs. You can describe methods, instance variables, constants and their types, and relationships such as inheritance and mixins. RBS is designed to support common patterns in Ruby programs and provides features such as union types, method overloads, and generics. In addition, "interface type" supports duck typing. Ruby 3.0 includes rbs gem, a library for processing type definitions written in this RBS language.
In other words, to summarize it very easily
--Define the type of Ruby program in RBS. --You can also define the structure of the program. --RBS is included from Ruby 3.0.
sig/user.rbs
class User
attr_reader name : String
attr_reader age : Integer
def initialize : (name: String, age: Integer) -> nil
end
It also defines the type of main.rb.
sig/main.rbs
class Object
private
def main: -> nil
end
Since I was able to write the type information, I will check it with steep.
shell
$ bundle exec steep init
Steepfile
# target :lib do
# signature "sig"
#
# check "lib" # Directory name
# check "Gemfile" # File name
# check "app/models/**/*.rb" # Glob
# # ignore "lib/templates/*.rb"
#
# # library "pathname", "set" # Standard libraries
# # library "strong_json" # Gems
# end
# target :spec do
# signature "sig", "sig-private"
#
# check "spec"
#
# # library "pathname", "set" # Standard libraries
# # library "rspec"
# end
target :lib do
signature "sig"
check "lib"
check "main.rb"
end
I specified the type information in the sig
directory and the check targets in the lib
directory and main.rb
.
shell
$ bundle exec steep check
If you exit without displaying anything, there is no problem.
main.rb
require "./lib/user"
def main
user = User.new(name: "John", age: "20") #Change age to String
puts user.name
end
if __FILE__ == $0
main
end
shell
$ bundle exec steep check
main.rb:4:37: IncompatibleAssignment: lhs_type=::Integer, rhs_type=::String ("20")
::String <: ::Integer
::Object <: ::Integer
::BasicObject <: ::Integer
==> ::BasicObject <: ::Integer does not hold
zsh: exit 1 bundle exec steep check
The mounting location and contents are displayed. I implemented a simple implementation of type information and checking. Next, we will use TypeProf, which makes it easier to write type information.
TypeProf
TypeProf is a type analysis tool included in the Ruby package. The current main use of TypeProf is a type of type inference. Enter plain Ruby code without type annotations, parse what methods are defined and used, and generate a prototype of the type signature in RBS format.
Since the installation is complete, I will use it immediately.
shell
$ bundle exec typeprof lib/user.rb
# Classes
class User
attr_reader name: untyped
attr_reader age: untyped
def initialize: (name: untyped, age: untyped) -> [untyped, untyped]
end
In this way, it will be automatically generated in RBS format to some extent from the implementation. However, it is untyped
because it is not possible to know which type is passed to name
or age
only by the definition of User class.
Now, let's run typeprof on main.rb
, which is actually using the User class.
shell
$ bundle exec typeprof main.rb
# Classes
class Object
private
def main: -> nil
end
class User
attr_reader name: String
attr_reader age: Integer
def initialize: (name: String, age: Integer) -> [String, Integer]
end
In main.rb
,"John"
is actually specified for name
and 20
is specified for age
, so the type information of the User class is accurate. However, the return value of initialize
is[String, Integer]
. In fact, the expected return value is nil
, so we'll modify the implementation.
lib/user.rb
class User
def initialize(name:, age:)
@name, @age = name, age
return #Clarify that there is no return value by describing return
end
attr_reader :name, :age
end
Try running typeprof again.
shell
$ bundle exec typeprof main.rb
# Classes
class Object
private
def main: -> nil
end
class User
attr_reader name: String
attr_reader age: Integer
def initialize: (name: String, age: Integer) -> nil
end
The return value of initialize
is now nil
!
▼ Repository of this content https://github.com/naro143/ruby-static-program-analysis-trial
I'm happy to see the addition of static analysis capabilities to Ruby. I'm not used to the description of RBS, but if I use TypeProf well, it seems to be easier to some extent. I'm also happy that the implementation will be more explicit so that TypeProf can be generated correctly. Personally, there is no problem with the behavior such as "hit a method that does not exist and nil is returned", but it is also static that implementations that are not intended and multiple candidates are proposed at the time of definition jump. I hope it will be improved by the analysis.