Install via RubyGems.
$ gem install steep
Steep requires Ruby 2.5.
Steep does not infer types from Ruby programs, but requires declaring types and writing annotations. You have to go on the following three steps.
Declare types in .rbi
files in sig
directory.
class Person
@name: String
@contacts: Array<Email | Phone>
def initialize: (name: String) -> any
def name: -> String
def contacts: -> Array<Email | Phone>
def guess_country: -> (String | nil)
end
class Email
@address: String
def initialize: (address: String) -> any
def address: -> String
end
class Phone
@country: String
@number: String
def initialize: (country: String, number: String) -> any
def country: -> String
def number: -> String
def self.countries: -> Hash<String, String>
end
- You can use simple generics, like
Hash<String, String>
. - You can use union types, like
Email | Phone
. - You have to declare not only public methods but also private methods and instance variables.
- You can declare singleton methods, like
self.countries
. - There is
nil
type to represent nullable types.
Write Ruby code with annotations.
class Person
# `@dynamic` annotation is to tell steep that
# the `name` and `contacts` methods are defined without def syntax.
# (Steep can skip checking if the methods are implemented.)
# @dynamic name, contacts
attr_reader :name
attr_reader :contacts
def initialize(name:)
@name = name
@contacts = []
end
def guess_country()
contacts.map do |contact|
# With case expression, simple type-case is implemented.
# `contact` has type of `Phone | Email` but in the `when` clause, contact has type of `Phone`.
case contact
when Phone
contact.country
end
end.compact.first
end
end
class Email
# @dynamic address
attr_reader :address
def initialize(address:)
@address = address
end
def ==(other)
# `other` has type of `any`, which means type checking is skipped.
# No type errors can be detected in this method.
other.is_a?(self.class) && other.address == address
end
def hash
self.class.hash ^ address.hash
end
end
class Phone
# @dynamic country, number
def initialize(country:, number:)
@country = country
@number = number
end
def ==(other)
# You cannot use `case` for type case because `other` has type of `any`, not a union type.
# You have to explicitly declare the type of `other` in `if` expression.
if other.is_a?(Phone)
# @type var other: Phone
other.country == country && other.number == number
end
end
def hash
self.class.hash ^ country.hash ^ number.hash
end
end
Run steep check
command to type check. ๐ก
$ steep check lib
lib/phone.rb:46:0: MethodDefinitionMissing: module=::Phone, method=self.countries (class Phone)
You now find Phone.countries
method is not implemented yet. ๐
You can use steep scaffold
command to generate a signature declaration.
$ steep scaffold lib/*.rb
class Person
@name: any
@contacts: Array<any>
def initialize: (name: any) -> Array<any>
def guess_country: () -> any
end
class Email
@address: any
def initialize: (address: any) -> any
def ==: (any) -> any
def hash: () -> any
end
class Phone
@country: any
@number: any
def initialize: (country: any, number: any) -> any
def ==: (any) -> void
def hash: () -> any
end
It prints all methods, classes, instance variables, and constants. It can be a good starting point to writing signatures.
Because it just prints all def
s, you may find some odd points:
- The type of
initialize
inPerson
looks strange. - There are no
attr_reader
methods extracted.
Generally, these are by our design.
steep check
is the command to run type checking.
Use -I
option to specify signature file or signature directory.
$ steep check -I my-types.rbi test.rb
If you don't specify -I
option, it assumes sig
directory.
When Steep finds an expression which cannot be typed, it assumes the type of the node is any. any type does not raise any type error so that fallback to any may hide some type errors.
Using --fallback-any-is-error
option prints the fallbacks.
$ steep check --fallback-any-is-error test.rb
When you are debugging, printing all types of all node in the source code may help.
Use --dump-all-types
for that.
$ steep check --dump-all-types test.rb
Try -v
option to report more information about type checking.
You can pass -G
option to specify name of gems to load type definitions.
$ steep check -G strong_json lib
When you are using bundler, Steep load type definitions from bundled gems automatically.
$ bundle exec steep check lib
To disable automatic gem detection from bundler, you can specify --no-bundler
option.
$ bundle exec steep check --no-bundler -G strong_json lib
Put your type definition file in a directory, ship that in your gem, and let metadata
of the gemspec to contain "steep_types" => dir_name
.
spec.metadata = { "steep_types" => "sig" }
We recommend using sig
as a name of the directory for type definitions, but you can use any directory.
You can find examples in smoke
directory.
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/soutaro/steep.