Convert Figma logo to code with AI

soutaro logosteep

Static type checker for Ruby

1,366
83
1,366
138

Top Related Projects

1,948

Type Signature for Ruby

3,602

A fast, powerful type checker designed for Ruby

The gem that has been saving people from typos since 2014

Quick Overview

Steep is a type checker for Ruby that uses the RBS (Ruby signature) format to define type signatures for Ruby programs. It provides static type checking for Ruby code, helping to catch type-related errors during development rather than at runtime.

Pros

  • Improved Code Quality: Steep's type checking helps identify type-related bugs early in the development process, leading to more robust and reliable Ruby applications.
  • Easier Refactoring: The type information provided by Steep makes it easier to refactor code, as the type checker can help ensure that changes don't break existing functionality.
  • Better Documentation: The RBS format used by Steep provides a way to document the types of variables, methods, and classes in a Ruby codebase, improving code readability and maintainability.
  • Interoperability with Other Tools: Steep can be integrated with other Ruby tooling, such as code editors and linters, to provide a seamless type-checking experience.

Cons

  • Learning Curve: Adopting Steep and the RBS format requires developers to learn a new set of concepts and tools, which can be a barrier to entry for some teams.
  • Overhead: Incorporating Steep into a Ruby project adds an additional step to the development workflow, which may be seen as an overhead by some teams.
  • Limited Ecosystem: Compared to some other programming languages, the ecosystem of RBS-annotated libraries and frameworks for Ruby is still relatively small, which can make it challenging to use Steep in some projects.
  • Potential Performance Impact: Depending on the size and complexity of a Ruby codebase, the type-checking performed by Steep may have a noticeable impact on the overall performance of the application.

Code Examples

Here are a few examples of how Steep can be used to type-check Ruby code:

# Example 1: Defining a type signature for a simple function
# RBS file
module Example
  def self.greet(name: String): String
    "Hello, #{name}!"
  end
end

# Ruby file
# @type method Example.greet: (name: String) -> String
puts Example.greet(name: "Alice")  # Output: Hello, Alice!
puts Example.greet(name: 42)       # Error: Argument 'name' expects String but got Integer
# Example 2: Defining a type signature for a class
# RBS file
class User
  attr_reader name: String
  attr_accessor age: Integer

  def initialize(name: String, age: Integer)
    @name = name
    @age = age
  end

  def birthday
    @age += 1
  end
end

# Ruby file
user = User.new(name: "Alice", age: 30)
user.name   # => "Alice"
user.age    # => 30
user.birthday
user.age    # => 31
user.name = 42 # Error: Assigning 'Integer' to '@name' (String)
# Example 3: Defining a type signature for a method with a union type
# RBS file
class StringOrNumber
  def self.to_s(value: String | Integer): String
    value.to_s
  end
end

# Ruby file
StringOrNumber.to_s("hello")  # => "hello"
StringOrNumber.to_s(42)       # => "42"
StringOrNumber.to_s(true)     # Error: Argument 'value' expects String or Integer but got Boolean

Getting Started

To get started with Steep, follow these steps:

  1. Install the steep gem:

    gem install steep
    
  2. Create an RBS file to define the type signatures for your Ruby code. For example, save the following in a file named example.rbs:

    module Example
      def self.greet(name: String): String
        "Hello, #{name}!"
      end
    end
    
  3. Create a Ruby file that uses the types defined in the RBS file. For example, save the following in a file named example.rb:

    # @type method Example.greet: (name: String) -> String
    puts Example.greet(name: "
    

Competitor Comparisons

1,948

Type Signature for Ruby

Pros of RBS

  • Official Ruby project, ensuring long-term support and integration with the language
  • More comprehensive type definitions, covering a wider range of Ruby's standard library
  • Better documentation and community resources

Cons of RBS

  • Primarily focused on type definitions, lacking built-in type checking functionality
  • Steeper learning curve for users new to type systems in Ruby
  • Less mature tooling ecosystem compared to Steep

Code Comparison

RBS syntax:

class User
  attr_reader name: String
  attr_reader age: Integer

  def initialize: (name: String, age: Integer) -> void
  def adult?: -> bool
end

Steep syntax:

class User
  attr_reader :name
  attr_reader :age

  def initialize(name:, age:)
  end

  def adult?
  end
end

RBS focuses on type definitions, while Steep combines type annotations with existing Ruby code. RBS provides a separate file for type definitions, whereas Steep allows inline type checking within Ruby files.

Both projects aim to improve type safety in Ruby, but they approach the problem differently. RBS is more about providing a standardized way to define types, while Steep offers a complete type checking solution. The choice between them depends on specific project needs and preferences.

3,602

A fast, powerful type checker designed for Ruby

Pros of Sorbet

  • Faster type checking and better performance for large codebases
  • More extensive documentation and community support
  • Gradual typing approach with configurable strictness levels

Cons of Sorbet

  • Requires more setup and configuration compared to Steep
  • Can be more intrusive, requiring type annotations in some cases
  • Limited support for certain Ruby metaprogramming features

Code Comparison

Steep:

# @type var x: Integer
x = 1

Sorbet:

T.let(x, Integer)
x = 1

Both Steep and Sorbet are static type checkers for Ruby, aiming to improve code quality and catch errors early. Steep offers a simpler setup process and is less intrusive, making it easier to integrate into existing projects. It also provides better support for certain Ruby-specific features.

Sorbet, developed by Stripe, offers superior performance for large codebases and has more extensive documentation. It provides a gradual typing approach, allowing developers to incrementally add type annotations. Sorbet also offers more advanced features like runtime type checking and IDE integration.

The code comparison shows the difference in syntax for type annotations. Steep uses comments for type declarations, while Sorbet introduces new syntax with the T.let method.

Ultimately, the choice between Steep and Sorbet depends on project requirements, team preferences, and the desired level of type checking strictness.

The gem that has been saving people from typos since 2014

Pros of did_you_mean

  • Integrated into Ruby core, providing out-of-the-box functionality
  • Offers real-time suggestions for misspelled method names and variables
  • Lightweight and requires minimal setup

Cons of did_you_mean

  • Limited to runtime error detection and suggestions
  • Does not perform static type checking or analysis
  • Focuses primarily on name-based corrections rather than type-related issues

Code Comparison

did_you_mean:

def hello_world
  hello_wrld
end
# NameError: undefined local variable or method `hello_wrld' for main:Object
# Did you mean?  hello_world

steep:

# @type var x: Integer
x = "string"
# Type error: type `String` does not match with `Integer`

Summary

did_you_mean is a runtime error suggestion tool integrated into Ruby, offering immediate feedback for misspellings and similar errors. It's easy to use but limited in scope.

Steep, on the other hand, is a static type checker for Ruby, providing more comprehensive type analysis and error detection before runtime. It requires more setup and annotation but offers stronger type safety guarantees.

The choice between the two depends on the specific needs of the project and the desired level of type checking and error prevention.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

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:

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.

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, 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, country, 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.

Docs

There are some documents in the manul and guide directories.

The doc directory contains a few internal design docs.

Examples

You can find examples in smoke directory.

IDEs

Steep implements some of the Language Server Protocol features.

  • For VSCode please install the plugin.
  • For SublimeText please install LSP package and follow instructions.
  • For Vim or Neovim please install ALE. 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.

# 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.

Contributing

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