Coder Social home page Coder Social logo

sord's Introduction

Sord

Overview

Sord is a Sorbet and YARD crossover. It can automatically generate RBI and RBS type signature files by looking at the types specified in YARD documentation comments.

If your project is already YARD documented, then this can generate most of the type signatures you need!

Sord is the perfect way to jump-start the adoption of types in your project, whether you plan to use Sorbet's RBI format or Ruby 3/Steep's RBS format.

Try Sord online at: sord.aaronc.cc

Sord has the following features:

  • Automatically generates signatures for modules, classes and methods
  • Support for multiple parameter or return types (T.any/|)
  • Gracefully handles missing YARD types (T.untyped/untyped)
  • Can infer setter parameter type from the corresponding getter's return type
  • Recognises mixins (include and extend)
  • Support for generic types such as Array<T> and Hash<K, V>
  • Can infer namespaced classes ([Bar] can become GemName::Foo::Bar)
  • Handles return types which can be nil (T.nilable/untyped)
  • Handles duck types (T.untyped/untyped)
  • Support for ordered list types ([Array(Integer, Symbol)] becomes [Integer, Symbol])
  • Support for boolean types ([true, false] becomes T::Boolean/bool)
  • Support for &block parameters documented with @yieldparam and @yieldreturn

Usage

Install Sord with gem install sord.

Sord is a command line tool. To use it, open a terminal in the root directory of your project and invoke sord, passing a path where you'd like to save your file (this file will be overwritten):

sord defs.rbi

Sord will generate YARD docs and then print information about what it's inferred as it runs. It is best to fix any issues in the YARD documentation, as any edits made to the resulting file will be replaced if you re-run Sord.

The output type is inferred by the file extension you use, but you can also specify it explicitly with --rbi or --rbs.

Shipping RBI Types

RBI files generated by Sord can be used in two main ways:

Generally, you should ship the type signatures with your gem if possible. sorbet-typed is meant to be a place for gems that are no longer updated or where the maintainer is unwilling to ship type signatures with the gem itself.

Flags

Sord also takes some flags to alter the generated file:

  • --rbi/--rbs: Override the output format inferred from the file extension.
  • --no-sord-comments: Generates the file without any Sord comments about warnings/inferences/errors. (The original file's comments will still be included.)
  • --no-regenerate: By default, Sord will regenerate a repository's YARD docs for you. This option skips regenerating the YARD docs.
  • --break-params: Determines how many parameters are necessary before the signature is changed from a single-line to a multi-line block. (Default: 4)
  • --replace-errors-with-untyped: Uses T.untyped instead of SORD_ERROR_* constants.
  • --replace-unresolved-with-untyped: Uses T.untyped when Sord is unable to resolve a constant.
  • --include-messages and --exclude-messages: Used to filter the logging messages given by Sord. --include-messages acts as a whitelist, printing only messages of the specified logging kinds, whereas --exclude-messages acts as a blacklist and suppresses the specified logging kinds. Both flags take a comma-separated list of logging kinds, for example omit,infer. When using --include-messages, the done kind is included by default. (You cannot specify both --include-messages and --exclude-messages.)
  • --exclude-untyped: Exclude methods and attributes with untyped return values.
  • --tags TAGS: Provide a list of comma-separated tags as understood by the yard command. E.g. `--tags 'mytag:My Description,mytag2:My New Description'

Example

Say we have this file, called test.rb:

module Example
  class Person
    # @param name [String] The name of the Person to create.
    # @param age [Integer] The age of the Person to create.
    # @return [Example::Person]
    def initialize(name, age)
      @name = name
      @age = age
    end

    # @return [String]
    attr_accessor :name

    # @return [Integer]
    attr_accessor :age

    # @param possible_names [Array<String>] An array of potential names to choose from.
    # @param possible_ages [Array<Integer>] An array of potential ages to choose from.
    # @return [Example::Person]
    def self.construct_randomly(possible_names, possible_ages)
      Person.new(possible_names.sample, possible_ages.sample)
    end
  end
end

First, generate a YARD registry by running yardoc test.rb. Then, we can run sord test.rbi to generate the RBI file. (Careful not to overwrite your code files! Note the .rbi file extension.) In doing this, Sord prints:

[INFER] Assuming from filename you wish to generate in RBI format
[DONE ] Processed 8 objects (2 namespaces and 6 methods)

The test.rbi file then contains a complete RBI file for test.rb:

# typed: strong
module Example
  class Person
    # _@param_ `name` — The name of the Person to create.
    # 
    # _@param_ `age` — The age of the Person to create.
    sig { params(name: String, age: Integer).void }
    def initialize(name, age); end

    # _@param_ `possible_names` — An array of potential names to choose from.
    # 
    # _@param_ `possible_ages` — An array of potential ages to choose from.
    sig { params(possible_names: T::Array[String], possible_ages: T::Array[Integer]).returns(Example::Person) }
    def self.construct_randomly(possible_names, possible_ages); end

    sig { returns(String) }
    attr_accessor :name

    sig { returns(Integer) }
    attr_accessor :age
  end
end

If we had instead generated test.rbs, we would get this file in RBS format:

module Example
  class Person
    # _@param_ `name` — The name of the Person to create.
    # 
    # _@param_ `age` — The age of the Person to create.
    def initialize: (String name, Integer age) -> void

    # _@param_ `possible_names` — An array of potential names to choose from.
    # 
    # _@param_ `possible_ages` — An array of potential ages to choose from.
    def self.construct_randomly: (Array[String] possible_names, Array[Integer] possible_ages) -> Example::Person

    attr_accessor name: String

    attr_accessor age: Integer
  end
end

Things to be aware of

The general rule of thumb for type conversions is:

  • If Sord understands the YARD type, then it is converted into the RBI or RBS type.
  • If the YARD type is missing, Sord fills in T.untyped.
  • If the YARD type can't be understood, Sord creates an undefined Ruby constant with a similar name to the unknown YARD type. For example, the obviously invalid YARD type A%B will become a constant called SORD_ERROR_AB. You should search through your resulting file to find and fix and SORD_ERRORs.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/AaronC81/sord. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

While contributing, if you want to see the results of your changes to Sord you can use the examples:seed Rake task. The task uses Sord to generate types for a number of open source Ruby gems, including Bundler, Haml, Rouge, and RSpec. rake examples:seed (and rake examples:reseed to regenerate the files) will clone the repositories of these gems into sord_examples/ and then generate the files into the same directory.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Sord project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

sord's People

Contributors

aaronc81 avatar connorshea avatar kirbycool avatar lukad avatar matmorel avatar ohai avatar shoma07 avatar solawing avatar tarellel avatar trashhalo avatar wagenet avatar yratanov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sord's Issues

Arrays that can have multiple options are output with invalid rbi signatures

Describe the bug
Arrays that can have multiple options are output with invalid rbi signatures.

To Reproduce

# Gets the users, channels, roles and emoji from a string.
# @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
# @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
# @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
def parse_mentions(mentions, server = nil)

Expected behavior
T::Array can only have one argument, so I think it should use T.any?

sig { params(mentions: String, server: T.nilable(Server)).returns(T::Array[T.any(User, Channel, Role, Emoji)]) }
def parse_mentions(mentions, server = nil); end

Actual behavior

sig { params(mentions: String, server: T.nilable(Server)).returns(T::Array[User, Channel, Role, Emoji]) }
def parse_mentions(mentions, server = nil); end

Which causes an error like so:

srb/discord.rbi:2952: Too many arguments provided for method T::Array.[]. Expected: 1, got: 3 https://srb.help/7004
    2952 |  sig { params(role: T.any(Role, T::Array[Role, String, Integer], String, Integer), reason: String).void }

Additional information
It seems to me this is caused by these lines:

if SORBET_SUPPORTED_GENERIC_TYPES.include?(generic_type)
"T::#{generic_type}[#{
split_type_parameters(type_parameters).map { |x| yard_to_sorbet(x, item) }.join(', ')}]"

Probably just need a special case for Array?

Crash when missing parameter name

Describe the bug
If a @param tag has no name corresponding to an argument, Sord crashes.

To Reproduce
Run Sord on this file:

class A
  # @param [String]
  # @return [void]
  def x(a); end
end

Expected behavior
Sord finishes, either inferring the missing parameter name to be a (as there is only one parameter), or generating a warning and typing a as T.untyped.

Actual behavior
Sord crashes with this exception:

[ERROR] undefined method `gsub' for nil:NilClass
         /var/lib/gems/2.5.0/gems/commander-4.4.7/lib/commander/user_interaction.rb:359:in `method_missing'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:157:in `block (3 levels) in add_methods'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:157:in `each'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:157:in `find'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:157:in `block (2 levels) in add_methods'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:155:in `map'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:155:in `block in add_methods'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:127:in `each'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:127:in `add_methods'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:251:in `add_namespace'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:268:in `block in generate'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:268:in `each'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:268:in `generate'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/lib/sord/rbi_generator.rb:284:in `run'
         /var/lib/gems/2.5.0/gems/sord-0.7.1/exe/sord:71:in `block (2 levels) in <top (required)>'
         /var/lib/gems/2.5.0/gems/commander-4.4.7/lib/commander/command.rb:182:in `call'
         /var/lib/gems/2.5.0/gems/commander-4.4.7/lib/commander/command.rb:153:in `run'
         /var/lib/gems/2.5.0/gems/commander-4.4.7/lib/commander/runner.rb:446:in `run_active_command'
         /var/lib/gems/2.5.0/gems/commander-4.4.7/lib/commander/runner.rb:71:in `run!'
         /var/lib/gems/2.5.0/gems/commander-4.4.7/lib/commander/delegates.rb:15:in `run!'
         /var/lib/gems/2.5.0/gems/commander-4.4.7/lib/commander/import.rb:5:in `block in <top (required)>'

Additional information
I found this when testing Sord on the Gosu library, where this syntax is used for attr_writers:

##
# Sets the playback volume, in the range [0.0; 1.0], where 0 is completely silent and 1 is full volume. Values outside of this range will be clamped to [0.0; 1.0].
# @param [Float]
# @return [Float]
attr_writer :volume

Elem constant generation

Is your feature request related to a problem? Please describe.
A lot of the remaining errors returned by rake examples:typecheck are to do with classes descending from Array or Struct, and thus needing to have the Elem constant redeclared.

sord_examples/yard.rbi:4248: Type Elem declared by parent Array must be re-declared in YARD::Parser::Ruby::Legacy::TokenList https://srb.help/5036
    4248 |        class TokenList < Array
                  ^^^^^^^^^^^^^^^^^^^^^^^
    https://github.com/sorbet/sorbet/tree/7afa6f30e4d5f6e6e49815ff130d6c98563df809/rbi/core/array.rbi#L7: Elem declared in parent here
     7 |  Elem = type_member(:out)
          ^^^^^^^^^^^^^^^^^^^^^^^^

sord_examples/yard.rbi:4296: Type Elem declared by parent Array must be re-declared in YARD::Parser::Ruby::Legacy::StatementList https://srb.help/5036
    4296 |        class StatementList < Array
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    https://github.com/sorbet/sorbet/tree/7afa6f30e4d5f6e6e49815ff130d6c98563df809/rbi/core/array.rbi#L7: Elem declared in parent here
     7 |  Elem = type_member(:out)
          ^^^^^^^^^^^^^^^^^^^^^^^^

sord_examples/yard.rbi:4361: Type Elem declared by parent Array must be re-declared in YARD::Parser::Ruby::AstNode https://srb.help/5036
    4361 |      class AstNode < Array
                ^^^^^^^^^^^^^^^^^^^^^
    https://github.com/sorbet/sorbet/tree/7afa6f30e4d5f6e6e49815ff130d6c98563df809/rbi/core/array.rbi#L7: Elem declared in parent here
     7 |  Elem = type_member(:out)
          ^^^^^^^^^^^^^^^^^^^^^^^^

sord_examples/yard.rbi:4501: Type Elem declared by parent YARD::Parser::Ruby::AstNode must be re-declared in YARD::Parser::Ruby::ReferenceNode https://srb.help/5036
    4501 |      class ReferenceNode < AstNode
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    sord_examples/yard.rbi:4361: Elem declared in parent here
    4361 |      class AstNode < Array
                ^^^^^^^^^^^^^^^^^^^^^

Describe the solution you'd like
The simplest solution would be to just define the necessary constants for each parent class that needs this (Array, Struct, etc.), but that's somewhat fragile. I'm not sure if there's a way for us to determine what Sorbet wants us to add when generating the rbi file?

I suppose these constants wouldn't change very often for core classes like this, so it may be fine to just handle it on a case-by-case basis in Sord.

Describe alternatives you've considered
I'd prefer if Sorbet magically fixed this itself, but I don't think it can :(

Keyword arguments get two colons in generated rbi signature

Input:

# Blah
# @param name [String] The field's name
# @param value [String] The field's value
# @param inline [true, false] Whether the field should be inlined
def add_field(name: nil, value: nil, inline: nil)
  # code
end

Generated rbi:

sig { params(name:: T.untyped, value:: T.untyped, inline:: T.untyped).void }

RFC: Breaking the bulk of RBI generation out into a separate gem

I had the idea that the RBI generation component of this gem could be broken out into its own gem with a polished interface. This would then ease the development of other RBI generator gems like Sord. (My main motivation for this is that I'd like to write a generator for Sequel models.) Sord could then use this gem for its generator.

Here's an example of how this interface could look:

# You can use a traditional OOP style...
builder = RbiGenerator::Builder.new
mod = builder.root.create_module('MyModule')
mod.create_class('Foo')
bar = mod.create_class('Bar', superclass: 'Foo')
bar.create_method(
  :do_something_to_string,
  parameters: [['x', 'String']],
  returns: 'String'
)

# ...or a more Ruby-esque style, as constructors `yield_self` to their block
builder = RbiGenerator::Builder.new
builder.root.create_module('MyModule') do |mod|
  mod.create_class('Foo')
  mod.create_class('Bar', superclass: 'Foo') do |bar|
    bar.create_method(
      :do_something_to_string,
      parameters: [['x', 'String']],
      returns: 'String'
    )
  end
end

This gem could also double as a kind of 'build system for RBIs', in that it can invoke multiple scripts which generate RBIs and pass them all the same RbiGenerator::Builder instance, creating one final RBI file for all generators. If multiple generators create different methods for the same class, they are merged together. If they attempt to generate different signatures for the same method, it could handle conflict resolution somehow.

@connorshea - what are your thoughts on this?

Asterisk parameters are lost

Describe the bug
Apparently Ruby accepts * as a parameter, for some reason. Sord doesn't currently handle this correctly.

To Reproduce
Run Sord on a file like so (these two methods are from optparse.rb https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb):

# typed: true
# test
module Test
  def self.incompatible_argument_styles(*)
  end

  def convert(opt = nil, val = nil, *)
  end
end

Expected behavior

# typed: strong
module Test
  sig { params(_: T.untyped).returns(T.untyped) }
  def self.incompatible_argument_styles(*_); end

  sig { params(opt: T.untyped, val: T.untyped, _: T.untyped).returns(T.untyped) }
  def convert(opt = nil, val = nil, *_); end
end

Actual behavior

# typed: strong
module Test
  sig { returns(T.untyped) }
  def self.incompatible_argument_styles; end

  sig { params(opt: T.untyped, val: T.untyped).returns(T.untyped) }
  def convert(opt = nil, val = nil); end
end

The reason I say it should return *_ is because that's how it was resolved in sorbet. That's just a limitation of Sorbet since the params would strip the *, so it needs to be represented as *_.

Handle @option tags

Is your feature request related to a problem? Please describe.
Selenium's Ruby gem (it's in the rb/ directory) has YARD docs for an options hash:

# Create a new Wait instance
#
# @param [Hash] opts Options for this instance
# @option opts [Numeric] :timeout (5) Seconds to wait before timing out.
# @option opts [Numeric] :interval (0.2) Seconds to sleep between polls.
# @option opts [String] :message Exception mesage if timed out.
# @option opts [Array, Exception] :ignore Exceptions to ignore while polling (default: Error::NoSuchElementError)
def initialize(opts = {})
  @timeout  = opts.fetch(:timeout, DEFAULT_TIMEOUT)
  @interval = opts.fetch(:interval, DEFAULT_INTERVAL)
  @message  = opts[:message]
  @ignored  = Array(opts[:ignore] || Error::NoSuchElementError)
end

Code here

But Sord doesn't handle this correctly.

class Wait
  sig { params(opts: Hash).returns(Wait) }
  def initialize(opts = {}); end

Describe the solution you'd like
I'm not 100% sure whether Sorbet has a good way to output the exact attributes of a hash inline, but it'd be nice if we could generate proper type information from this kind of YARD doc.

Additional context

Implicit nilable when generating method signatures

Is your feature request related to a problem? Please describe.

If I have a method like this:

def initialize(text: nil, icon_url: nil); end

If there's no YARD documentation, the generated type signature looks like this:

sig { params(text: T.untyped, icon_url: T.untyped).returns(T.untyped) }
def initialize(text: nil, icon_url: nil); end

I'm wondering if Sord should being able to assume the type is nilable given that the default value of the method is nil.

Describe the solution you'd like
If nil is the default value for a parameter, should Sord automatically generate the type as T.nilable(T.untyped) or is that outside its scope?

If it does that, it should do the same even if the YARD docs do specify a type, e.g.

# In:
# ----
# @param text [String]
def initialize(text: nil); end

# Out:
# ----
sig { params(text: T.nilable(String)).returns(T.untyped)
def initialize(text: nil); end

Literal symbols aren't handled by Sord

Describe the bug
I'm not actually sure if this is valid YARD, it may not be. If it isn't, we can close this.

For a Paginator, direction can either be :up or :down. Sord isn't able to determine a type from this, maybe it should be able to?

To Reproduce

# Creates a new {Paginator}
# @param limit [Integer] the maximum number of items to request before stopping
# @param direction [:up, :down] the order in which results are returned in
# @yield [Array, nil] the last page of results, or nil if this is the first iteration.
#   This should be used to request the next page of results.
# @yieldreturn [Array] the next page of results
def initialize(limit, direction, &block)
  # code
end

Expected behavior

sig { params(limit: Integer, direction: Symbol, block: T.untyped).returns(Paginator) }
def initialize(limit, direction, &block); end

Actual behavior
SORD_ERROR_up and SORD_ERROR_down:

# sord warn - ":up" does not appear to be a type
# sord warn - ":down" does not appear to be a type
sig { params(limit: Integer, direction: T.any(SORD_ERROR_up, SORD_ERROR_down), block: T.untyped).returns(Paginator) }
def initialize(limit, direction, &block); end

Additional information
I imagine this'd also happen when given any kinds of literals, e.g. if an attribute could only ever be 1, 2, 3, 4, or 5.

Initialize methods should return void

Is your feature request related to a problem? Please describe.
Sord generates a type signature for initialize methods that returns the parent class, this isn’t what’s recommended by the Sorbet docs. The docs recommend returning void to discourage users from using initialize directly.

Describe the solution you'd like
Sord should generate initialize methods with void return types. We could also potentially have a flag that would return to the current behavior? (Or vice versa)

https://sorbet.org/docs/faq#how-do-i-write-a-signature-for-initialize

Proper support for blocks

Is your feature request related to a problem? Please describe.
Blocks are currently always T.untyped, which limits how useful Sorbet can be when checking their types.

Describe the solution you'd like
Sord should be able to read @yieldparam and @yieldreturn tags in YARD docs to generate accurate type signatures for yielding methods.

Add --clean flag to output rbi file without comments

Is your feature request related to a problem? Please describe.
I want to use the generated rbi file, so I want to be able to create one without all the comments.

Describe the solution you'd like
I'd like sord to be able to output an rbi file without all the comments, so I can use it for sorbet-typed or in a gem.

I'm not in love with --clean as the name, if you can come up with a better name I'd be fine with that.

What's currently generated:

# sord omit - no YARD type given for "token", using T.untyped
# sord omit - no YARD type given for "server_id", using T.untyped
# sord omit - no YARD type given for "name", using T.untyped
# sord omit - no YARD type given for "colour", using T.untyped
# sord omit - no YARD type given for "hoist", using T.untyped
# sord omit - no YARD type given for "mentionable", using T.untyped
# sord omit - no YARD type given for "packed_permissions", using T.untyped
# sord omit - no YARD type given for "reason", using T.untyped
sig { params(token: T.untyped, server_id: T.untyped, name: T.untyped, colour: T.untyped, hoist: T.untyped, mentionable: T.untyped, packed_permissions: T.untyped, reason: T.untyped).void }
def create_role(token, server_id, name, colour, hoist, mentionable, packed_permissions, reason = nil); end

What I'd like:

sig { params(token: T.untyped, server_id: T.untyped, name: T.untyped, colour: T.untyped, hoist: T.untyped, mentionable: T.untyped, packed_permissions: T.untyped, reason: T.untyped).void }
def create_role(token, server_id, name, colour, hoist, mentionable, packed_permissions, reason = nil); end

Describe alternatives you've considered
Manually cleaning up the comments, but this feels silly :)

Additional context
It'd also be nice to have linebreaks between methods for readability. Maybe also a configurable limit where the signature becomes a block when there are more than n params (I'm not sure if there should be a separate issue for formatting?), e.g:

sig do
  params(
    token: T.untyped,
    server_id: T.untyped,
    name: T.untyped,
    colour: T.untyped,
    hoist: T.untyped,
    mentionable: T.untyped,
    packed_permissions: T.untyped,
    reason: T.untyped
  ).void
end
def create_role(token, server_id, name, colour, hoist, mentionable, packed_permissions, reason = nil); end

Add Travis CI

We should be running the test suite in CI for Sord.

Sord generates an invalid rbi file for haml

Describe the bug
haml.rbi has an invalid method in it for some reason:

sig { params(index: T.untyped).returns(T.untyped) }
def rstrip_buffer!(index = -1)); end

To Reproduce
Run sord on haml and then run srb tc on the resulting rbi.

Expected behavior
It'd output a valid rbi file.

Actual behavior
It outputs this method with an extra closing parenthesis:

sig { params(index: T.untyped).returns(T.untyped) }
def rstrip_buffer!(index = -1)); end

Additional information
The problem isn't with Haml's method, as far as I can tell it's valid:

# Get rid of and whitespace at the end of the buffer
# or the merged text
def rstrip_buffer!(index = -1)
  last = @to_merge[index]
  if last.nil?
    push_silent("_hamlout.rstrip!", false)
    return
  end

  case last.first
  when :text
    last[1] = last[1].rstrip
    if last[1].empty?
      @to_merge.slice! index
      rstrip_buffer! index
    end
  when :script
    last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
    rstrip_buffer! index - 1
  else
    raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Compiler@to_merge.")
  end
end

https://github.com/haml/haml/blob/a054e2ad7964f894fce032737f9a4f0e146700a5/lib/haml/compiler.rb#L308

Class<...> generic is not supported

Describe the bug
Some projects (namely Rouge) use a Class<...> type in their YARD documentation. This is not supported by Sord currently.

To Reproduce
Use the Class<...> type in a YARD type, for example, Class<String>.

Expected behavior
A T.class_of(...) call is generated in the signature.

Actual behavior
SORD_ERROR_Class, with a warning message unsupported generic type "Class" in "Class<...>".

If Sord finds 0 objects, suggest generating YARD docs

Is your feature request related to a problem? Please describe.
If I haven't run yard first to generate the YARD files, Sord will just output 0 objects.

Describe the solution you'd like
If this happens, I'd like to have Sord tell me a possible solution: running yard to generate the documentation. Something like "Are you sure you've generated the YARD docs for this project? Run 'yard' to generate the YARD docs."

Additional context
Is there any way to check whether the YARD docs are the most recent version of the repository? It'd be nice to at least give the user a warning in that case. For a while I was using an older version of discordrb's YARD docs, because I'm an idiot :)

Print list of SORD_ERRORs at the end of sord output

Is your feature request related to a problem? Please describe.
Currently you're just supposed to look through the generated file for SORD_ERRORs. This is tedious and I assume a lot of people may not do it.

Describe the solution you'd like
I'd prefer if sord surfaced the error messages at the end of the command line output for easy reference.

Additional context
Maybe some general statistics besides just the number of parsed objects would be nice to have?

More readable coloring for logging output

Describe the bug
The text for method names (e.g. Rails::Generators::Actions#rails_command) is white/gray, which causes the text to be effectively unreadable on a white background.

To Reproduce
Look at this screenshot:

Screen Shot 2019-06-22 at 9 10 26 PM

Expected behavior
I expect to be able to read text output.

Actual behavior
I cannot.

Splat parameters are hardcoded as 'args'

Describe the bug
Sord generates a signature for methods with a splat parameter (e.g. *args) with args as the name no matter what.

To Reproduce

class Event
  class << self
    protected

    # Lorem ipsum
    # @param methods [Array<Symbol>] The methods to delegate.
    # @param hash [Hash<Symbol => Symbol>] A hash with one `:to` key and the value the method to be delegated to.
    def delegate(*methods, hash)
      # code
    end
  end
end

Expected behavior
The splat parameter should be called methods.

sig { params(methods: T::Array[T.any], hash: T::Hash[Symbol, Symbol]).void }
def self.delegate(*methods, hash); end

Actual behavior
Instead it's called args:

sig { params(args: T::Array[T.any], hash: T::Hash[Symbol, Symbol]).void }
def self.delegate(*methods, hash); end

Additional information
args is hardcoded here:

"args: T::Array[T.any]"

Don't add unnecessary params() when there are none

e.g. it creates things like this:

sig { params().returns(Symbol) }
def target_type() end

There's also a few examples of these in the README example. When there aren't any params, the signature can just be:

sig { returns(Symbol) }

Incorrect handling of keyword arguments with default values

The input looks like this:

def add_field(name: nil, value: nil, inline: nil)
  # code
end

The generated RBI looks like this:

def add_field(name: = nil, value: = nil, inline: = nil) end

It shouldn't have the = when it's a keyword argument :)

Tests for RbiGenerator

Is your feature request related to a problem? Please describe.
There aren't any tests for Sord::RbiGenerator, which makes ensuring you haven't broken anything difficult.

Describe the solution you'd like
A decent test suite needs to be written for RbiGenerator.

Additional context
Writing tests for this may be somewhat tricky, because they rely heavily on the contents of the YARD registry. It might be necessary to create a tiny framework to generate 'stub' YARD registries for each test.

Sord repeats parent class methods in each child class

Describe the bug
This is probably more of a problem with YARD than Sord, but if you give it a file where Class A has methods, and then Class B is a child class of Class A, Class B will also have those methods. Sorbet can figure this out on its own, so it's not really necessary.

To Reproduce
Run optparser.rb through Sord and see the various child classes of ParseError.

Something like this:

class A
  class ParseError < RuntimeError
    def initialize(*args)
      # code
    end

    def recover(argv)
      # code
    end
  end

  class NeedlessArgument < ParseError; end

  class MissingArgument < ParseError; end

  class InvalidOption < ParseError; end
end

Expected behavior

I'd expect it not to output a bunch of repetitive methods if they aren't redefined in the child classes:

class ParseError < RuntimeError
  sig { params(args: T.untyped).void }
  def initialize(*args); end

  sig { params(argv: T.untyped).returns(T.untyped) }
  def recover(argv); end
end

class NeedlessArgument < A::ParseError; end

class MissingArgument < A::ParseError; end

class InvalidOption < A::ParseError; end

Actual behavior

class ParseError < RuntimeError
  sig { params(args: T.untyped).void }
  def initialize(*args); end

  sig { params(argv: T.untyped).returns(T.untyped) }
  def recover(argv); end
end

class NeedlessArgument < A::ParseError
  sig { params(args: T.untyped).void }
  def initialize(*args); end

  sig { params(argv: T.untyped).returns(T.untyped) }
  def recover(argv); end
end

class MissingArgument < A::ParseError
  sig { params(args: T.untyped).void }
  def initialize(*args); end

  sig { params(argv: T.untyped).returns(T.untyped) }
  def recover(argv); end
end

class InvalidOption < A::ParseError
  sig { params(args: T.untyped).void }
  def initialize(*args); end

  sig { params(argv: T.untyped).returns(T.untyped) }
  def recover(argv); end
end

Method tag with no return type

Describe the bug

Invoking sord on graphql-ruby docs leads to runtime crash.

To Reproduce

gem install sord
git clone https://github.com/rmosolgo/graphql-ruby.git
cd graphql-ruby
git ch v1.9.6
bundle install
bundle exec yard
sord graphql.rbi

Expected behavior

The program finished successfully.

Actual behavior

[ERROR] undefined method `first' for nil:NilClass
         /Users/eloy/.gem/ruby/2.5.3/gems/commander-4.4.7/lib/commander/user_interaction.rb:359:in `method_missing'
         /Users/eloy/.gem/ruby/2.5.3/gems/sord-0.6.0/lib/sord/rbi_generator.rb:120:in `block in add_methods'

Additional information

I was able to get sord to successfully finish by changing this line to:

elsif return_tags.length == 1 && return_tags.first.types&.first&.downcase == "void"

However, I have spent all of 5 seconds on this after first installing sord, so not sure if this leads to the desired behaviour.

Range isn't handled properly if it doesn't have a type

Is your feature request related to a problem? Please describe.
YARD uses Range in quite a few places in its docs and it doesn't supply a type for the Range (e.g. Range<Integer>), it just uses Range.

sord_examples/yard.rbi:17064: Malformed type declaration. Generic class without type arguments Range https://srb.help/5004
       17064 |        sig { returns(Range) }

Describe the solution you'd like
Range should be set to T::Range[T.untyped] if there's no type defined for the value. I think the code is supposed to do this, but Range isn't picked up by the GENERIC_TYPE_REGEX.

I assume this is true of most of the other generics supported by Sorbet.

Support YARD's ordered list syntax: Array<(String, Fixnum, Hash)>

Is your feature request related to a problem? Please describe.
YARD supports a syntax involving parenthesis for Arrays that have fixed, ordered values: Array<(String, Fixnum, Hash)>

Input:

# @return [Array(Integer, Integer)] the current shard key
attr_reader :shard_key

Current Sord output:

# sord warn - "Array(Integer, Integer)" does not appear to be a type
sig { returns(SORD_ERROR_ArrayIntegerInteger) }
def shard_key(); end

Describe the solution you'd like
Sord should be able to understand this syntax and turn it into a Sorbet type.

I think Tuples fit here as the Sorbet equivalent: https://sorbet.org/docs/tuples

This creates a fixed array type (also referred to as a tuple), which is a fixed-length array with known types for each element. For example, [String, T.nilable(Float)] validates that an object is an array of exactly length 2, with the first item being a String and the second item being a Float or nil.

So given this input:

# @return [Array(Integer, Integer)] the current shard key
attr_reader :shard_key

You'd want this output:

sig { returns([Integer, Integer]) }
def shard_key(); end

The YARD documentation mentions only Array<(String, Integer)>, but it seems like Array(String, Integer) is also perfectly valid. The YARD types parser seems to handle this weirdly (but also that repo hasn't had a commit in nearly 10 years, so it may not be the best thing to work off of here).

Sord Hash syntax doesn't return valid Sorbet type

Describe the bug
Sord doesn't handle a YARD hash like {String => Symbol} correctly. It generates invalid Sorbet.

To Reproduce
Run Sord on a file like this:

module A
  # @return [{String => Symbol}]
  def x; end
end

Expected behavior

# typed: strong
module A
  sig { returns(T::Hash[String, Symbol]) }
  def x(); end
end

Actual behavior

# typed: strong
module A
  sig { returns(T::Hash<String, Symbol>) }
  def x(); end
end

Additional information
I wrote a spec to test this:

it 'handles a hash correctly' do
  YARD.parse_string(<<-RUBY)
    module A
      # @return [{String => Symbol}]
      def x; end
    end
  RUBY

  expect(subject.generate.strip).to eq fix_heredoc(<<-RUBY)
    # typed: strong
    module A
      sig { returns(T::Hash[String, Symbol]) }
      def x(); end
    end
  RUBY
end

Not handling Arrays or Hashes without type name

Describe the bug
Sord doesn't correctly handle <String> (should be equivalent to Array<String>) or {String => true} (equivalent to Hash{String => true})

I tested sord with zeitwerk and discovered this issue.

To Reproduce

# Absolute paths of files or directories that have to be preloaded.
#
# @private
# @return [<String>]
attr_reader :preloads

Expected behavior

sig { returns(T::Array[String]) }
def preloads(); end

Actual behavior

# sord warn - "<String>" does not appear to be a type
sig { returns(SORD_ERROR_String) }
def preloads(); end

Additional information
The type parser seems to suggest these are valid, so we should try to support them.

Figure out how to get gems using custom YARD formatters to work with Sord

Describe the bug
I'm not sure if this is only the case with the Rake task? Regardless, yard should be run with system so it'll output any errors when running Sord.

To Reproduce
It seems like any gem that uses a custom formatter (kramdown, redcarpet, etc.) for their YARD docs will fail. This is why rouge, haml, and oga all fail.

[error]: Missing 'redcarpet' gem for Markdown formatting. Install it with `gem install redcarpet`
# or
[error]: Missing 'kramdown' gem for Markdown formatting. Install it with `gem install kramdown`

Expected behavior
Any gem with YARD docs should work fine with Sord.

Actual behavior
Some gems, like haml and rouge, don't seem to work properly when Sord is run on them. This is despite the fact that yaml should be installed for them.

Additional information
I think we need to somehow force bundle install to install gems in all groups, but I'm not sure how exactly to get it to do that. It doesn't seem like there's a flag for it.

Automatic parameter reordering

Is your feature request related to a problem? Please describe.
I wanted to use Sord to generate an RBI for graphql-ruby however a lot of the methods in it have required named parameters before optional named parameters.

This is a problem because Sorbet requires that method parameters are written in a specific order, see also sorbet/sorbet#1273

For example (see on sorbet.run):

class VariablesAreUsedAndDefinedError < GraphQL::StaticValidation::Error
  sig do
    params(
      message: T.untyped,
      path: T.untyped,
      nodes: T.untyped,
      name: T.untyped,
      error_type: T.untyped
    ).returns(VariablesAreUsedAndDefinedError)
  end
  def initialize(message, path: nil, nodes: [], name:, error_type:); end
end

Describe the solution you'd like
I'd like for Sord to reorder the parameters for me when generating the RBI file.

So, for the example above it'd be output like:

sig do
  params(
    message: T.untyped,
    name: T.untyped,
    error_type: T.untyped
    path: T.untyped,
    nodes: T.untyped,
  ).returns(VariablesAreUsedAndDefinedError)
end
def initialize(message, name:, error_type:, path: nil, nodes: []); end

We'd need to make sure not to reorder parameters where order actually matters (e.g. non-keyword parameters).

Describe alternatives you've considered
I've considered updating all the methods in graphql-ruby to order their parameters correctly, but since this would only be helpful for Sorbet I don't know if it'd be merged, plus it probably effects other gems as well 🤷‍♂

Additional context
See this Gist for the RBI generated by Sord: https://gist.github.com/connorshea/6dad1ba6f60e8fb67c8cf5556188eb1c

Some methods (without YARD docblock?) are given `sig { void }` and it's incorrect.

Describe the bug
Methods without YARD docblock is given sig { void } and it's incorrect.
(Note: I'm not familiar with YARD so my understanding of what a YARD docblock is may be wrong)

To Reproduce

  1. Git clone Rails
  2. cd rails/activerecord/
  3. Run sord activerecord.rbi
  4. Checkout activerecord.rbi, search for changed_attribute_names_to_save
  5. Confirm that it has { void } signature
    Look at the implementation, it is wrong:
    https://github.com/rails/rails/blob/3b2ba908bad6962be8b1b0c368a97721dc864eb4/activerecord/lib/active_record/attribute_methods/dirty.rb
    I observe this happens to a lot of methods.

Expected behavior
It should best omit the method
Or it should be typed as returns(T.untyped)

Actual behavior
The method has sig { void }

Additional information
It's more important to be correct than being comprehensive but wrong, since sorbet will enforce the sig at run-time

I'm very excited about using sord to generate rbi files for sorbet-typed. If you need any help getting it to a reliable state, please feel free to let me know. I'll chime in where I can.

Decouple RbiGenerator from Commander

Is your feature request related to a problem? Please describe.
RbiGenerator's constructor currently expects an instance of a class in the Commander gem, which isn't ideal, since it should be independent of the CLI.

Describe the solution you'd like
Alter RbiGenerator's constructor to not rely on a Commander-generated class.

Flag to ignore certain error types

Is your feature request related to a problem? Please describe.
I'd like to run Sord and only have comments for SORD_ERRORs and other warn-level errors. Right now there are a lot of messages about OMITs, but I only care about WARNs.

There's a lot of stuff like this that isn't very useful for me:

# sord omit - no YARD type given for "gem_names", using T.untyped
# sord omit - no YARD return type given, using T.untyped
sig { params(gem_names: T.untyped).returns(T.untyped) }
def dependency_specs(gem_names); end

# sord omit - no YARD type given for "gem_names", using T.untyped
# sord omit - no YARD return type given, using T.untyped
sig { params(gem_names: T.untyped).returns(T.untyped) }
def unmarshalled_dep_gems(gem_names); end

Describe the solution you'd like
There are three main ideas I have for implementing this:

  • Exclude messages of certain types: sord test.rbi --exclude-messages=omit,duck
  • Include messages of certain types: sord test.rbi --include-messages=warn
  • Error level: sord test.rbi --error-level=warn

The first two could both be implemented, if we really want to.

The 'error level' flag would require that we decide what the 'level's of each error type are, and since we have omit / duck / infer / warn, that wouldn't really make sense.

Additional context
I assume the flags would effect both the logging and code comments, having two separate flags for the logging and the code comments would unnecessarily complicate things.

Sord fails when run on discordrb

sord-0.2.0/lib/sord/logging.rb:13:in `silent?': uninitialized class variable @@silent in Sord::Logging (NameError)

I ran it on a local copy of discordrb since it has YARD documentation, it didn't like that very much :P

Connors-MacBook-Pro:discordrb connorshea$ gem install sord
Fetching sord-0.2.0.gem
Fetching colorize-0.8.1.gem
Successfully installed colorize-0.8.1
Successfully installed sord-0.2.0
Parsing documentation for colorize-0.8.1
Installing ri documentation for colorize-0.8.1
Parsing documentation for sord-0.2.0
Installing ri documentation for sord-0.2.0
Done installing documentation for colorize, sord after 0 seconds
2 gems installed
Connors-MacBook-Pro:discordrb connorshea$ sord defs.rbi
Traceback (most recent call last):
        14: from /Users/connorshea/.rbenv/versions/2.6.2/bin/sord:23:in `<main>'
        13: from /Users/connorshea/.rbenv/versions/2.6.2/bin/sord:23:in `load'
        12: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/exe/sord:4:in `<top (required)>'
        11: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:125:in `run'
        10: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:125:in `each'
         9: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:130:in `block in run'
         8: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:58:in `add_methods'
         7: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:58:in `each'
         6: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:71:in `block in add_methods'
         5: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:71:in `map'
         4: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:71:in `each'
         3: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:93:in `block (2 levels) in add_methods'
         2: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/logging.rb:82:in `omit'
         1: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/logging.rb:35:in `generic'
/Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/logging.rb:13:in `silent?': uninitialized class variable @@silent in Sord::Logging (NameError)
Did you mean?  silent=
               silent?
        7: from /Users/connorshea/.rbenv/versions/2.6.2/bin/sord:23:in `<main>'
        6: from /Users/connorshea/.rbenv/versions/2.6.2/bin/sord:23:in `load'
        5: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/exe/sord:4:in `<top (required)>'
        4: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:118:in `run'
        3: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/rbi_generator.rb:151:in `rescue in run'
        2: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/logging.rb:60:in `error'
        1: from /Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/logging.rb:37:in `generic'
/Users/connorshea/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/sord-0.2.0/lib/sord/logging.rb:13:in `silent?': uninitialized class variable @@silent in Sord::Logging (NameError)

I was at the v3.3.0 tag for the discordrb repo, if that matters at all.

Also, you might want to add a GitHub issue template :)

Method aliases

Describe the bug
Method aliases aren't handled in any special way by sord, which can cause typechecking errors with Sorbet.

To Reproduce

# @return [Integer, nil] the colour of the bar to the side, in decimal form
attr_reader :colour
alias_method :color, :colour

# Sets the colour of the bar to the side of the embed to something new.
# @param value [String, Integer, {Integer, Integer, Integer}, #to_i, nil] The colour in decimal, hexadecimal, R/G/B decimal, or nil to clear the embeds colour
#   form.
def colour=(value)
  if value.nil?
    @colour = nil
  elsif value.is_a? Integer
    raise ArgumentError, 'Embed colour must be 24-bit!' if value >= 16_777_216

    @colour = value
  elsif value.is_a? String
    self.colour = value.delete('#').to_i(16)
  elsif value.is_a? Array
    raise ArgumentError, 'Colour tuple must have three values!' if value.length != 3

    self.colour = value[0] << 16 | value[1] << 8 | value[2]
  else
    self.colour = value.to_i
  end
end

alias_method :color=, :colour=

Expected behavior
No errors.

Actual behavior
I get these errors:

lib/discordrb/webhooks/embeds.rb:38: Redefining the existing method Discordrb::Webhooks::Embed#color as a method alias https://srb.help/5037
    38 |    alias_method :color, :colour
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    srb/discord.rbi:6491: Previous definition
    6491 |  def color(); end
            ^^^^^^^^^^^

lib/discordrb/webhooks/embeds.rb:61: Redefining the existing method Discordrb::Webhooks::Embed#color= as a method alias https://srb.help/5037
    61 |    alias_method :color=, :colour=
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    srb/discord.rbi:6497: Previous definition
    6497 |  def color=(); end
            ^^^^^^^^^^^^

Additional information
This is the rbi file that's generated:

class Discordrb::Webhooks::Embed 
  sig { returns(T.nilable(Integer)) }
  def colour(); end
  sig { returns(T.nilable(Integer)) }
  def color(); end
  # sord warn - "{Integer, Integer, Integer}" does not appear to be a type
  # sord ducl - #to_i looks like a duck type, replacing with T.untyped
  sig { params(value: T.nilable(T.any(String, Integer, SORD_ERROR_IntegerIntegerInteger, T.untyped))).void }
  def colour=(value); end
  sig { void }
  def color=(); end
end

Some signatures are broken due to lack of module scoping

Describe the bug
Because Sord does not nest modules and classes inside each other in the RBI files, module and class names must always be fully qualified.

To Reproduce

module Example
  class A; end

  class B
    # @return [A]
    def x
      # ...
    end
  end
end

Expected behavior
Sorbet accepts this code.

Actual behavior
Sorbet encounters a reference error because it cannot resolve A. If Example::A is used as B#x's return type, such an error does not occur.

Additional information
This will require arbitrarily deep nesting of modules and classes, and the RBI generator is not capable of handling the indentation for this. It may be best to generate the file with no indentation to begin with, then pass a beautifier over it. (This has the potential to kill two birds with one stone by implementing the parameter line breaking specified in #13.)

Git tags and Changelog.md

Is your feature request related to a problem? Please describe.
It'd be nice if the sord repo had git tags for each version (I think RubyGems might create them automatically when you publish a gem? git push --tags pushes any local tags to the remote), it'd also be nice to have a Changelog file in the repo. :)

Describe the solution you'd like
I usually just use the format from Keep a Changelog (e.g. https://github.com/connorshea/vscode-ruby-test-adapter/blob/master/CHANGELOG.md), I'd recommend it.

Formatting improvements

Is your feature request related to a problem? Please describe.
I'd like to see the formatting of code output by sord be improved.

I know this is mentioned in #13, but I wanted to have a separate issue for it - plus I have another suggestion to add to what's mentioned in that issue.

Describe the solution you'd like
Generally, sord outputs code that looks like the following:

# typed: strong
module Discordrb
  sig { params(one_id: T.untyped, other: T.untyped).void }
  def self.id_compare(one_id, other); end
  sig { params(msg: String).returns(T::Array[String]) }
  def self.split_message(msg); end
module API
  sig { returns(String) }
  def api_base(); end
  sig { returns(String) }
  def self.api_base(); end
module User
  sig { params(token: T.untyped, user_id: T.untyped).void }
  def resolve(token, user_id); end
  sig { params(token: T.untyped, user_id: T.untyped).void }
  def self.resolve(token, user_id); end
end
end
end

However, I'd like it to do three main things to make the generated rbi files more readable:

  • Linebreaks between signature/method pairs
  • Linebreaks between modules/classes
  • Indent the code properly when nesting modules/classes

Like this:

# typed: strong
module Discordrb
  sig { params(one_id: T.untyped, other: T.untyped).void }
  def self.id_compare(one_id, other); end

  sig { params(msg: String).returns(T::Array[String]) }
  def self.split_message(msg); end

  module API
    sig { returns(String) }
    def api_base(); end

    sig { returns(String) }
    def self.api_base(); end

    module User
      sig { params(token: T.untyped, user_id: T.untyped).void }
      def resolve(token, user_id); end

      sig { params(token: T.untyped, user_id: T.untyped).void }
      def self.resolve(token, user_id); end
    end
  end
end

I also wanted to suggest that the signature output by sord use block syntax after a certain number of parameters is reached (probably 4 or 5 by default?), preferably with this number being configurable using a command line flag.

e.g. instead of this:

sig { params(one: T.untyped, two: T.untyped, three: T.untyped, four: T.untyped, five: T.untyped).void }
def self.id_compare(one, two, three, four, five); end

sord would output this:

sig do
  params(
    one: T.untyped,
    two: T.untyped,
    three: T.untyped,
    four: T.untyped,
    five: T.untyped
  ).void
end
def self.id_compare(one, two, three, four, five); end

Describe alternatives you've considered
Running an auto-formatter rufo or rubocop on the code after its been generated is an option, and while it fixes the indentation problem, unfortunately neither has a concept of signatures so they don't really do sensible things when formatting code with them.

Rufo, for example, seems to group them in pairs of three or one?

module Discordrb
  sig { params(one_id: T.untyped, other: T.untyped).void }
  def self.id_compare(one_id, other); end
  sig { params(msg: String).returns(T::Array[String]) }
  def self.split_message(msg); end

  module API
    sig { returns(String) }

    def api_base(); end

    sig { returns(String) }
    def self.api_base(); end
    sig { params(value: String).void }

    def api_base=(value); end

    sig { params(value: String).void }
    def self.api_base=(value); end
    sig { returns(String) }

    def cdn_url(); end

    module User
      sig { params(token: T.untyped, user_id: T.untyped).void }

      def resolve(token, user_id); end
    end
  end
end

yieldparam of void incorrectly generates returns(T.untyped)

Describe the bug
A yieldparam of [void] generates T.proc.returns(T.untyped) instead of T.proc.void.

I actually discovered this in Sord itself, the self.add_hook method in logging.rb has this problem.

To Reproduce

module A
  # @yieldparam [Symbol] foo
  # @yieldreturn [void]
  # @return [void]
  def self.foo(&blk); end
end

Expected behavior

# typed: strong
module A
  sig { params(blk: T.proc.params(foo: Symbol).void).void }
  def self.foo(&blk); end
end

Actual behavior

# typed: strong
module A
  sig { params(blk: T.proc.params(foo: Symbol).returns(T.untyped)).void }
  def self.foo(&blk); end
end

Additional information
I wrote a spec for this :)

it 'handles void yieldreturn' do
  YARD.parse_string(<<-RUBY)
    module A
      # @yieldparam [Symbol] foo
      # @yieldreturn [void]
      # @return [void]
      def self.foo(&blk); end
    end
  RUBY

  expect(subject.generate.strip).to eq fix_heredoc(<<-RUBY)
    # typed: strong
    module A
      sig { params(blk: T.proc.params(foo: Symbol).void).void }
      def self.foo(&blk); end
    end
  RUBY
end

Test result:

@@ -1,6 +1,6 @@
 # typed: strong
 module A
-  sig { params(blk: T.proc.params(foo: Symbol).void).void }
+  sig { params(blk: T.proc.params(foo: Symbol).returns(T.untyped)).void }
   def self.foo(&blk); end
 end

Sord strips important information from included constants

Describe the bug

In YARD, this file can be found at lib/yard/server/commands/list_command.rb:

# frozen_string_literal: true
module YARD
  module Server
    module Commands
      class ListCommand < LibraryCommand
        include Templates::Helpers::BaseHelper

        def run
          # code
        end
      end
    end
  end
end

To Reproduce
Run Sord on YARD and search for include BaseHelper in class ListCommand.

Expected behavior

class ListCommand < LibraryCommand
  include Templates::Helpers::BaseHelper

  # methods
end

Actual behavior
Just BaseHelper is generated, which causes an error:

class ListCommand < LibraryCommand
  include BaseHelper

  # methods
end

Handling duck typing

Is your feature request related to a problem? Please describe.
Currently, duck-typed parameters don't work and output a SORD_ERROR.

Input:

# Adds a new custom emoji on this server.
# @param name [String] The name of emoji to create.
# @param image [String, #read] A base64 encoded string with the image data, or an object that responds to `#read`, such as `File`.
# @param roles [Array<Role, String, Integer>] An array of roles, or role IDs to be whitelisted for this emoji.
# @param reason [String] The reason the for the creation of this emoji.
# @return [Emoji] The emoji that has been added.
def add_emoji(name, image, roles = [], reason: nil)
  # code
end

Output:

# sord warn - "#read" does not appear to be a type
# sord omit - no YARD type given for "reason:", using T.untyped
sig { params(name: String, image: T.any(String, SORD_ERROR_read), roles: T::Array[Role, String, Integer], reason: T.untyped).returns(Emoji) }
def add_emoji(name, image, roles = [], reason: nil); end

YARD supports them (kind of), but Sorbet doesn't really have a concept for duck types as far as I know. I guess they should be treated as untyped?

Describe the solution you'd like
I'm not really sure, there are a few options I can think of:

  • Add a warning ([DUCK ]?) and make the type of the parameter T.untyped.
  • Not allowing duck-typing, this is essentially where we're at right now, and it's not ideal.

Handling attr_reader/writer/accessor?

I noticed that resolv.rbi in sorbet-typed has signatures like so:

sig { returns(Integer) }
attr_reader :priority

sig { returns(Integer) }
attr_reader :weight

sig { returns(Integer) }
attr_reader :port

sig { returns(Resolv::DNS::Name) }
attr_reader :target

I wonder if it'd be possible to do this with Sord, since it seems like Sorbet can handle things with reader/writers instead of Sord needing to.

Support for &block

Describe the bug
Methods with a &block parameter end up with an invalid signature in the generated rbi.

To Reproduce
This is the input code (most is the YARD docs are irrelevant to the issue, I've already cut out a lot of the docs for this method):

# Adds a new command to the container.
# @param name [Symbol, Array<Symbol>] The name of the command to add, or an array of multiple names for the command
# @param attributes [Hash] The attributes to initialize the command with.
# @option attributes [Integer] :permission_level The minimum permission level that can use this command, inclusive.
#   See {CommandBot#set_user_permission} and {CommandBot#set_role_permission}.
# @yield The block is executed when the command is executed.
# @yieldparam event [CommandEvent] The event of the message that contained the command.
# @note `LocalJumpError`s are rescued from internally, giving bots the opportunity to use `return` or `break` in
#   their blocks without propagating an exception.
# @return [Command] The command that was added.
def command(name, attributes = {}, &block)
  # code
end

Expected behavior
It should match the block example in the sorbet documentation: https://sorbet.org/docs/sigs#params-annotating-parameter-types

It should probably look like this, where the parameter name is block instead of &block:

sig { params(name: T.any(Symbol, T::Array[Symbol]), attributes: Hash, block: T.untyped).returns(Command) }
def command(name, attributes = {}, &block); end

Actual behavior
It generates &block: T.untyped in the signature, which is invalid:

sig { params(name: T.any(Symbol, T::Array[Symbol]), attributes: Hash, &block: T.untyped).returns(Command) }
def command(name, attributes = {}, &block); end

Escaped symbol arguments aren't handled correctly

Escaped symbols, or string-symbols, or as I like to call them, 'abominations', don't get sent through to the generated file properly.

Input:

def create_server(name, region = :'eu-central')
  # code
end

Generated rbi method:

def create_server(name, region = eu-central) end

Add flag to replace SORD_ERRORs with untyped

Is your feature request related to a problem? Please describe.
I’d like to be able to automatically generate a valid rbi without needing any manual changes.

Describe the solution you'd like to happen

I’d like a flag, like --replace-errors-with-untyped, to automatically set T.untyped in place of SORD_ERRORs

HereDoc constant generation causes syntax errors in generated RBIs

Describe the bug
If a constant's value is a HereDoc, the generated RBI contains syntax errors due to the weird syntax of HereDocs in method calls. (I found this when generating an RBI for Rack.)

To Reproduce
Generate an RBI for this code:

module Foo
  CONST = <<-HERE
    This is a heredoc.
  HERE
end

Expected behavior

# typed: strong
module Foo
  CONST = T.let(<<-HERE, T.untyped)
    This is a heredoc.
  HERE
end

Actual behavior

# typed: strong
module Foo
  CONST = T.let(<<-HERE
  This is a heredoc.
HERE, T.untyped)
end

Sord doesn't handle Hash syntax like Hash{KeyType=>ValueType} correctly

For example, from discordrb:

# @return [Hash<String => Change>, RoleChange, nil] lorem ipsum
attr_reader :changes

Sord creates this:

# sord warn - "String => Change" does not appear to be a type
# sord warn - nil is probably not a type, but using anyway
sig { params().returns(T.any(T::Hash[SORD_ERROR_StringChange], RoleChange, nil)) }
def changes() end

YARD docs explanation of the syntax:

Hashes can be specified either via the parametrized type discussed above, in the form Hash<KeyType, ValueType>, or using the hash specific syntax: Hash{KeyTypes=>ValueTypes}. In the latter case, KeyTypes or ValueTypes can also be a list of types separated by commas.

It doesn't explicitly mention Hash<KeyType=>ValueType> but it seems like that's valid YARD as well.

Switch to Rainbow

Now that Sord depends on Parlour, it also depends on Rainbow. Which means that Sord depends on both Rainbow and Colorizer, and that's silly. We should switch to just Rainbow.

Add flag for setting unresolved constants to T.untyped

Is your feature request related to a problem? Please describe.

When generating an rbi for YARD using Sord, a lot of the errors when running srb tc yard.rbi are about constants that couldn't be resolved:

yard.rbi:3332: Unable to resolve constant OptionParser https://srb.help/5002
    3332 |      sig { params(opts: OptionParser).void }

Describe the solution you'd like
I'd like a flag like --replace-unresolved-constants-with-untyped, that would replace these with T.untyped, just like --replace-errors-with-untyped does now.

Describe alternatives you've considered
Going through manually and replacing these with untyped.

Additional context
You can reproduce this by generating the rbi for yard (from sord_examples/yard: bundle exec sord ../yard.rbi --no-comments --replace-errors-with-untyped) and then running srb tc yard.rbi on the result. (note: you'll need #73 first or the generated rbi will be invalid)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.