• Stars
    star
    1,190
  • Rank 39,309 (Top 0.8 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 7 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Static type checker for Ruby

Steep - Gradual Typing for Ruby

Installation

Install via RubyGems.

$ gem install steep

Requirements

Steep requires Ruby 2.6 or later.

Usage

Steep does not infer types from Ruby programs, but requires declaring types and writing annotations. You have to go on the following three steps.

0. steep init

Run steep init to generate a configuration file.

$ steep init       # Generates Steepfile

Edit the Steepfile:

target :app do
  check "lib"
  signature "sig"

  library "set", "pathname"
end

1. Declare Types

Declare types in .rbs files in sig directory.

class Person
  @name: String
  @contacts: Array[Email | Phone]

  def initialize: (name: String) -> untyped
  def name: -> String
  def contacts: -> Array[Email | Phone]
  def guess_country: -> (String | nil)
end

class Email
  @address: String

  def initialize: (address: String) -> untyped
  def address: -> String
end

class Phone
  @country: String
  @number: String

  def initialize: (country: String, number: String) -> untyped
  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.

2. Write Ruby Code

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 `untyped`, 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
  attr_reader :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 `untyped`, 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

3. Type Check

Run steep check command to type check. πŸ’‘

$ steep check
lib/phone.rb:46:0: MethodDefinitionMissing: module=::Phone, method=self.countries (class Phone)

You now find Phone.countries method is not implemented yet. πŸ™ƒ

Prototyping signature

You can use rbs prototype command to generate a signature declaration.

$ rbs prototype rb lib/person.rb lib/email.rb lib/phone.rb
class Person
  @name: untyped
  @contacts: Array[untyped]
  def initialize: (name: untyped) -> Array[untyped]
  def guess_country: () -> untyped
end

class Email
  @address: untyped
  def initialize: (address: untyped) -> untyped
  def ==: (untyped) -> untyped
  def hash: () -> untyped
end

class Phone
  @country: untyped
  @number: untyped
  def initialize: (country: untyped, number: untyped) -> untyped
  def ==: (untyped) -> void
  def hash: () -> untyped
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 defs, you may find some odd points:

  • The type of initialize in Person looks strange.
  • There are no attr_reader methods extracted.

Generally, these are by our design.

rbs prototype offers options: rbi to generate prototype from Sorbet RBI and runtime to generate from runtime API.

Guides

There are some guides in the guide directory. I know we need more comprehensive set of documentations. Just started writing docs.

Examples

You can find examples in smoke directory.

IDEs

Steep implements some of the Language Server Protocol features.

Other LSP supporting tools may work with Steep where it starts the server as steep langserver.

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/soutaro/steep.

More Repositories

1

querly

Query Method Calls from Ruby Programs
Ruby
244
star
2

Align-text-baseline-sketch-plugin

Align text layer vertically based on its baseline and cap height
JavaScript
66
star
3

steep-vscode

VSCode extension for Steep
TypeScript
62
star
4

strong_json

Type check JSON objects
Ruby
22
star
5

cycromatic

Cyclomatic Complexity Calculator for Ruby
Ruby
16
star
6

hungry-delete.el

Delete following / preceeding white spaces
Emacs Lisp
15
star
7

vscode-rbs-syntax

RBS syntax highlight for VSCode.
TypeScript
13
star
8

nullarihyon

Nullability check for Objective-C implementation
Ruby
13
star
9

bundly

Bundle gems from YAML file
Ruby
9
star
10

SelectorKit

Objective-C
9
star
11

VirtualGesture

Cheat UIGestureRecognizer for testing
Objective-C
9
star
12

unification_assertion

Assertion to test unifiability of two values
Ruby
9
star
13

SMHTTPClient

HTTP/1.1 client, based on socket
Swift
8
star
14

sublime-rbs-plugin

Syntax definition of RBS
5
star
15

true_string

Translate a string to boolean
Ruby
5
star
16

Majima

Three-way merge for array and dictionary, in Swift
Swift
4
star
17

emacs-c-k

VSCode Extension to provide Emacs like C-k
TypeScript
4
star
18

chefpad

A client-server iPhone app example using Frank
JavaScript
3
star
19

Obihiro

Page Object for View Controller
Objective-C
3
star
20

contror

Ruby
3
star
21

Arelish

Arel like NSPredicate constructor
Objective-C
3
star
22

git_diff_map

Line mapping between original and new
Ruby
3
star
23

ast_utils

Utility over parser gem AST
Ruby
3
star
24

Ubiregi-API-Clients

Scala
2
star
25

rbswiki

Ruby
2
star
26

rc

Configuration files
Shell
2
star
27

hitokage

Faster Float#to_s
C++
2
star
28

rbs-docs

TypeScript
2
star
29

RubyEx

for Ruby programming excercise
Ruby
2
star
30

magellan

C
2
star
31

pragger

Ruby
1
star
32

rbs-src

Ruby
1
star
33

PromisingRouter

Deferred openURL: routing for iOS apps
Swift
1
star
34

jack_and_the_elastic_beanstalk

Ruby
1
star
35

async-test

Ruby
1
star
36

pronto-querly

Pronto runner for Querly
Ruby
1
star
37

jsonseq

Read/write JSON Text Sequence
Ruby
1
star
38

Git-Publisher-Test

1
star
39

ubiregi-api

JavaScript
1
star
40

hoge

PHP
1
star
41

QueryHelper

Objective-C
1
star
42

prefetch-test-unit

Improve performance of Rails apps' tests using Test::Unit
Ruby
1
star
43

rbs-inline

Inline RBS type declaration
Ruby
1
star