Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/soutaro/steep

Static type checker for Ruby
https://github.com/soutaro/steep

ruby typechecker

Last synced: 8 days ago
JSON representation

Static type checker for Ruby

Awesome Lists containing this project

README

        

# 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`:

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

library "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.

```rb
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 `def`s, 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.

* [Guides](guides)

## Examples

You can find examples in `smoke` directory.

## IDEs

Steep implements some of the Language Server Protocol features.
- For **VSCode** please install [the plugin](https://github.com/soutaro/steep-vscode).
- For **SublimeText** please install [LSP](https://github.com/sublimelsp/LSP) package and follow [instructions](https://lsp.sublimetext.io/language_servers/#steep).
- For **Vim** or **Neovim** please install [ALE](https://github.com/dense-analysis/ale?tab=readme-ov-file#asynchronous-lint-engine). You may want to `let g:ale_ruby_steep_executable = 'bundle'` to use your bundled `steep` version.

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

## Rake Tasks

Steep comes with a set of configurable Rake tasks.

```ruby
# Rakefile

require "steep/rake_task"
Steep::RakeTask.new do |t|
t.check.severity_level = :error
t.watch.verbose
end

task default: [:steep]
```

Use `bundle exec rake -T` to see all available tasks.

## 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](https://rubygems.org).

## Contributing

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