Git Product home page Git Product logo

parlour's Introduction

Parlour

Build Status Gem

Parlour is a Ruby type information generator, merger and parser, supporting both Sorbet RBI files and Ruby 3/Steep RBS files. It consists of three key parts:

  • The generator, which outputs beautifully formatted RBI/RBS files, created using an intuitive DSL.

  • The plugin/build system, which allows multiple Parlour plugins to generate RBIs for the same codebase. These are combined automatically as much as possible, but any other conflicts can be resolved manually through prompts.

  • The parser (currently RBI-only), which can read an RBI and convert it back into a tree of generator objects.

Why should I use this?

  • Parlour enables much easier creation of RBI/RBS generators, as formatting is all handled for you, and you don't need to write your own CLI.

  • You can use many plugins together seamlessly, running them all with a single command and consolidating all of their definitions into a single output file.

  • You can effortlessly build tools which need to access types within an RBI; no need to write your own parser!

  • You can generate RBI/RBS to ship with your gem for consuming projects to use (see "RBIs within gems" in Sorbet's docs).

Please read the wiki to get started!

Feature Support

Feature RBI RBS
GENERATION
Classes ⚠️ (missing extension)
Modules ⚠️ (missing extension)
Interfaces
Attributes
Methods
Overloaded methods ❌*
Structs ✅†
Enums ✅†
Generation with plugins
MANIPULATION
Parsing
Merging
  • ✅ - Well supported

  • ⚠️ - Some missing features

  • ❌ - Not currently supported

  • * Only supported in stdlib types anyway

  • † Not natively supported; available as a one-way conversion from RBI

Creating Type Information

Each file format has its own type information generator class, so there are two different generators you can use: RbiGenerator and RbsGenerator. Both generators are similar to use, however they provide different object types and parameters to match the functionality of their underlying type systems.

You can also convert your type information between formats; see converting between formats.

Using Just the Generator

Here's a quick example of how you can generate some type information. Here we'll generate an RBI using the RbiGenerator classes:

require 'parlour'

generator = Parlour::RbiGenerator.new
generator.root.create_module('A') do |a|
  a.create_class('Foo') do |foo|
    foo.create_method('add_two_integers', parameters: [
      Parlour::RbiGenerator::Parameter.new('a', type: 'Integer'),
      Parlour::RbiGenerator::Parameter.new('b', type: 'Integer')
    ], return_type: 'Integer')
  end

  a.create_class('Bar', superclass: 'Foo')
end

generator.rbi # => Our RBI as a string

This will generate the following RBI:

module A
  class Foo
    sig { params(a: Integer, b: Integer).returns(Integer) }
    def add_two_integers(a, b); end
  end

  class Bar < Foo
  end
end

Using the RBS generator looks similar, but has an intermediary MethodSignature class to support RBS' method overloading:

require 'parlour'

generator = Parlour::RbsGenerator.new
generator.root.create_module('A') do |a|
  a.create_class('Foo') do |foo|
    foo.create_method('add_two_integers', [
      Parlour::RbsGenerator::MethodSignature.new(
        [
          Parlour::RbsGenerator::Parameter.new('a', type: 'Integer'),
          Parlour::RbsGenerator::Parameter.new('b', type: 'Integer')
        ],
        'Integer'
      )
    ])
  end

  a.create_class('Bar', superclass: 'Foo')
end

generator.rbs # => Our RBS as a string

This generates an equivalent RBS file:

module A
  class Foo
    def add_two_integers: (Integer a, Integer b) -> Integer
  end

  class Bar < Foo
  end
end

Writing a Plugin

Plugins are better than using the generator alone, as your plugin can be combined with others to produce larger files without conflicts.

We could write the above example as an RBI plugin like this:

require 'parlour'

class MyPlugin < Parlour::Plugin
  def generate(root)
    root.create_module('A') do |a|
      a.create_class('Foo') do |foo|
        foo.create_method('add_two_integers', parameters: [
          Parlour::RbiGenerator::Parameter.new('a', type: 'Integer'),
          Parlour::RbiGenerator::Parameter.new('b', type: 'Integer')
        ], return_type: 'Integer')
      end

      a.create_class('Bar', superclass: 'Foo')
    end
  end
end

(Obviously, your plugin will probably examine a codebase somehow, to be more useful!)

You can then run several plugins, combining their output and saving it into one RBI file, using the command-line tool. The command line tool is configurated using a .parlour YAML file. For example, if that code was in a file called plugin.rb, then using this .parlour file and then running parlour would save the RBI into output.rbi:

output_file:
  rbi: output.rbi

relative_requires:
  - plugin.rb
  - app/models/*.rb

plugins:
  MyPlugin: {}

The {} indicates that this plugin needs no extra configuration. If it did need configuration, this could be specified like so:

plugins:
  MyPlugin:
    foo: something
    bar: something else

You can also use plugins from gems. If that plugin was published as a gem called parlour-gem:

output_file:
  rbi: output.rbi

requires:
  - parlour-gem

plugins:
  MyPlugin: {}

The real power of this is the ability to use many plugins at once:

output_file:
  rbi: output.rbi

requires:
  - gem1
  - gem2
  - gem3

plugins:
  Gem1::Plugin: {}
  Gem2::Plugin: {}
  Gem3::Plugin: {}

Currently, only plugins which generate RBI files are supported. However, you can use Parlour's type conversion to convert the RBI types into RBS types:

output_file:
  rbi: output.rbi
  rbs: output.rbs

Using Types

The most important part of your type information is the types themselves, which you'll be specifying for method parameters, method returns, and attributes. These include simple types like String, up to more complex types like "an array of elements which are one of Integer, String, or nil".

There are two ways to represent these types in Parlour:

  1. As generalized types; that is, instances of classes in the Parlour::Types namespace. This is the recommended way, as it is format-agnostic and can be compiled to RBI or RBS. For more information about these types and how to use them, see this wiki page.

  2. As strings of code written in the format that your type system expects. The given strings are directly inserted into the final type file. These types are not portable across formats, and as such are not recommended and may be phased out in the future.

Currently most type values within Parlour are typed as Types::TypeLike, which accepts a String or a Types::Type subclass.

include Parlour

# Two ways to express an attribute called 'example', which is:
#   an array of nilable strings or integers

# 1. With generalised types - type is agnostic to the underlying type system
root.create_attr_accessor('example', type:
  Types::Array.new(
    Types::Nilable.new(
      Types::Union.new(['String', 'Integer'])
    )
  )
)

# 2. With string types - format depends on type system
#    If using RBI...
root.create_attr_accessor('example', type:
  'T::Array[T.nilable(T.any(String, Integer))]'
)
#    If using RBS...
root.create_attr_accessor('example', type:
  'Array[?(String | Integer)]'
)

Generalizing String Types

If you have loaded an RBI project or created a structure of nodes on an RbiGenerator, you can use #generalize_from_rbi! on your root namespace to attempt to permanently convert the RBI string types into generalized types:

# Build up an RBI tree with string types
root.create_attr_accessor('example', type:
  'T::Array[T.nilable(T.any(String, Integer))]'
)

# Generalize it
root.generalize_from_rbi!

# Take a look at our generalized type!
pp root.children.first.type
# => #<Parlour::Types::Array:0x0000557cdcebfdf8
#     @element=
#      #<Parlour::Types::Nilable:0x0000557cdcef8c70
#       @type=
#        #<Parlour::Types::Union:0x0000557cdcea0a70
#         @types=
#          [#<Parlour::Types::Raw:0x0000557cdcea1920 @str="String">,
#           #<Parlour::Types::Raw:0x0000557cdcea0ae8 @str="Integer">]>>>

Parsing RBIs

You can either parse individual RBI files, or point Parlour to the root of a project and it will locate, parse and merge all RBI files.

Note that Parlour isn't limited to just RBIs; it can parse inline sigs out of your Ruby source too!

require 'parlour'

# Return the object tree of a particular file
Parlour::TypeLoader.load_file('path/to/your/file.rbis')

# Return the object tree for an entire Sorbet project - slow but thorough!
Parlour::TypeLoader.load_project('root/of/the/project')

The structure of the returned object trees is identical to those you would create when generating an RBI, built of instances of RbiObject subclasses.

Generating RBI for a Gem

Include parlour as a development_dependency in your .gemspec:

spec.add_development_dependency 'parlour'

Run Parlour from the command line:

bundle exec parlour

Parlour is configured to use sane defaults assuming a standard gem structure to generate an RBI that Sorbet will automatically find when your gem is included as a dependency. If you require more advanced configuration you can add a .parlour YAML file in the root of your project (see this project's .parlour file as an example).

To disable the parsing step entire and just run plugins you can set parser: false in your .parlour file.

Converting Between Formats

For more information, see the wiki page.

Currently, only RBI to RBS conversion is supported, and if you've used string types (or are using a freshly-loaded project) you must generalize them first.

Then, all you need to do is create an RbsGenerator (which the converter will add your converted types to) and a Conversion::RbiToRbs instance (which performs the conversion). Then you can convert each object at your RbiGenerator's root namespace:

rbi_gen = Parlour::RbiGenerator.new
# Then, after populating the RbiGenerator with types...

# Create an RbsGenerator and a converter
rbs_gen = Parlour::RbsGenerator.new
converter = Parlour::Conversion::RbiToRbs.new(rbs_gen)

# Convert each item at the root of the RbiGenerator and it to the root of the RbsGenerator
converter.convert_all(rbi_gen.root, rbs_gen.root)

Parlour Plugins

Have you written an awesome Parlour plugin? Please submit a PR to add it to this list!

  • Sord - Generate RBIs from YARD documentation
  • parlour-datamapper - Simple plugin for generating DataMapper model types
  • sorbet-rails - Generate RBIs for Rails models, routes, mailers, etc.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/AaronC81/parlour. 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.

After making changes, you may wish to regenerate the RBI definitions in the sorbet folder by running these srb rbi commands:

srb rbi gems
srb rbi sorbet-typed

You should also regenerate the parlour.rbi file by running bundle exec parlour. Don't edit this file manually, as your changes will be overwritten!

License

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

Code of Conduct

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

parlour's People

Contributors

aaronc81 avatar bmalinconico avatar bradleybuda avatar caiofilipemr avatar camertron avatar connorshea avatar dduugg avatar ghiculescu avatar jeffcarbs avatar katayounhillier avatar kirbycool avatar paracycle 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

Watchers

 avatar  avatar

parlour's Issues

Plugins need to be able to take configuration

Is your feature request related to a problem? Please describe.
While thinking of how to create a Sequel plugin, I realised that I needed a way of specifying the entry point for the application to the plugin.

Describe the solution you'd like
Plugins need to be able to take configuration options somehow. I think the most sensible way to do this is to have plugins defined in a YAML file, rather than as command-line options. Something like:

out: rbi/app.rbi

gems:
  - parlour-sequel

plugins:
  ParlourSequel:
    entry: src/app.rb

Invoking parlour would then load options from this file.

Describe alternatives you've considered
Each plugin could have its own configuration file, but this would be very awkward.

Make the first parameter of various method "name" without using keyword argument

Is your feature request related to a problem? Please describe.
This is a personal taste, but I feel the name: argument is overly verbose.

Consider:

root.create_module("FooModule")
vs
root.create_module(name: "FooModule")

The first option is just as clear. It's also more readable. I've forgotten the name: args a handful of time now. I'm sure I'll get used to it but it'd be nice to support the first syntax.

Describe the solution you'd like
Make name a normal argument instead of keyword arg.

Describe alternatives you've considered
If you don't want to drop name:, we can keep supporting name: and a first argument that'll be used as name
eg:

def create_module(dname=nil, name:)
  name = name || dname
end

But the first option is cleaner in the long term. There is only a few current users who need to codemod once.

Additional context
Add any other context about the feature request here.

Option to skip including parentheses when there aren't parameters

Normally parlour will output a method like this:

sig { void }
def parameterless(); end

But I'd like to output this instead:

sig { void }
def parameterless; end

Ruby also technically supports this, but it's blasphemy so we shouldn't support it:

def bad param, param2, param3
  puts 'why would you do this to me'
end

attr_reader/writer/accessor

I'd like a way to generate attr_reader, attr_writer, and attr_accessors in the RBIs generated by Parlour :)

Probably with create_attr_reader, create_attr_writer, and create_attr_accessor methods? Or add_* like add_include and add_extend (we should probably standardize all methods on one or the other, unless I'm missing some reason to use both create_ and add_).

Add support for generating and parsing method visibility

Is your feature request related to a problem? Please describe.
Brain Freeze currently generates RBIs for all methods, not just public ones. Fixing this would require Parlour being able to parse and consequently generate method visibility.

Describe the solution you'd like
Method RBI objects should have a visibility field, which is populated based on:

  • Class context changes, like private in a class body
  • Class calls with symbol arguments, like private :x, :y, :z in a class body
  • Class calls with a definition as an argument, like private def ... end

Add option to exclude methods without signatures

Parlour 3.0 adds features which mean that methods without signatures are detected by the TypeParser. It would be good to add an option which reverts to the old behaviour of these methods being ignored.

Un-pin Sorbet version

Describe the bug
The Parlour version is currently pinned in the gemspec due to type checking issues with the latest Sorbet. However, this version of Sorbet appears to be causing Parlour to not be working on some Macs, plus pinning versions isn't great for longevity, so it would be ideal to resolve these issues and un-pin it.

Better object printing

Is your feature request related to a problem? Please describe.
Parlour's current ability to print objects nicely is a bit dire. RbiObject#describe gives poor output which doesn't show any children, and output from '#inspect is incredibly noisy.

Describe the solution you'd like
It would be nice to have a way to pretty-print objects and their children.

Generated files don't end with a newline

Describe the bug
Generated RBI files don't end with a newline, which conflicts with most Ruby style guides and makes cat-ing them look ugly:

aaron $ cat out.rbi
# typed: true
class Foo
endaaron $ 

To Reproduce
Generate any RBI using Parlour and look at the end of the file.

Expected behavior
The RBIs should end with a newline.

Actual behavior
The RBI does not end with a newline.

Add a way to generate a "type_parameter" for a method

TypeLoader.load_project crashes on empty ruby source file

Describe the bug
I have a file in my source tree whose complete contents is:

# typed: strong

No, there's no good reason for this - now that I've discovered this file I've deleted it, but it is valid Ruby and sorbet has no problem with it.

When I run Parlour::TypeLoader.load_project('my/project/dir'), it fails with this error:

TypeError (Parameter 'ast': Expected type Parser::AST::Node, got type NilClass)
Caller: /Users/brad/src/parlour/lib/parlour/type_parser.rb:111
Definition: /Users/brad/src/parlour/lib/parlour/type_parser.rb:95

To Reproduce
This one-liner reproduces the issue for me:

Parlour::TypeLoader.load_source('# typed: strong')

Expected behavior
I assume this file would be ignored in the AST and processing would continue on the rest of the project without this file contributing anything? I'm brand new to Parlour, this might not be the correct expected behavior.

Actual behavior
Crash as described above

Additional information
If a maintainer can advise on the correct expected behavior, I might be able to put together a pull request to fix this. Thanks!

Add support for constants

There needs to be a way to add a constant definition to a Namespace. This will probably involve adding a new RbiObject for them, along with a create_constant method.

*** RuntimeError Exception: You declared `describe` as .override, but the method it overrides is not declared as `overridable`.

Describe the bug
I got this error when calling candidates.describe

*** RuntimeError Exception: You declared `describe` as .override, but the method it overrides is not declared as `overridable`.
  Parent definition: Parlour::RbiGenerator::RbiObject at /home/dev/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/parlour-0.5.1/lib/parlour/rbi_generator/rbi_object.rb:124
  Child definition:  Parlour::RbiGenerator::Method at /home/dev/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/parlour-0.5.1/lib/parlour/rbi_generator/method.rb:192

Allow arbitrary input to be added to a class/module

Is your feature request related to a problem? Please describe.
sorbet-rails generates some code like this:

class GameEngine < ApplicationRecord
  extend T::Sig
  extend T::Generic
  extend GameEngine::ModelRelationShared
  include GameEngine::InstanceMethods
  Elem = type_template(fixed: GameEngine)
end

This currently cannot be reproduced via Parlour, as far as I know.

Describe the solution you'd like
I'd like a method like add_code or add_arbitrary that'd take a String or HEREDOC (which I think is also just a string?) that would allow me to inject any arbitrary code into a class.

generator = Parlour::RbiGenerator.new
generator.root.create_class('GameEngine', superclass: 'ApplicationRecord') do |game_engine|
  add_extend('T::Sig')
  add_extend('T::Generic')
  add_extend('GameEngine::ModelRelationShared')
  add_include('GameEngine::InstanceMethods')
  add_arbitrary('Elem = type_template(fixed: GameEngine)')
end

generator.rbi
  # =>
  # class GameEngine < ApplicationRecord
  #   extend T::Sig
  #   extend T::Generic
  #   extend GameEngine::ModelRelationShared
  #   include GameEngine::InstanceMethods
  #   Elem = type_template(fixed: GameEngine)
  # end

Describe alternatives you've considered
I think it'd be useful to have an 'escape hatch' instead of needing to implement a generator for every possible piece of code in Parlour.

TypeLoader#load_project failing when a T::Struct has a nested class

Describe the bug
Parlour::TypeLoader.load_project fails when a T::Struct has a nested class defined outside of the original class body.

To Reproduce

namespace = Parlour::TypeLoader.load_source(<<-RUBY)
  class A < T::Struct; end
  class A::B; end
RUBY

# Called within `load_project`
Parlour::ConflictResolver.new.resolve_conflicts(namespace) do |n, o|
  raise "conflict of #{o.length} objects: #{n}"
end

Expected behavior
No conflict

Actual behavior
Conflict

Additional information
I think what's happening is that when the loader encounters A::B it creates a top-level A child but without a subclass so the conflict resolution thinks they're different. This doesn't happen when the original A is a subclass of some other class which makes me think this is specific to T::Struct. Some other things I tried:

Using the "expanded" nesting still doesn't work.

namespace = Parlour::TypeLoader.load_source(<<-RUBY)
  class A < T::Struct; end

  class A
    class B; end
  end
RUBY

Explicitly subclass from T::Struct again works

namespace = Parlour::TypeLoader.load_source(<<-RUBY)
  class A < T::Struct; end

  class A < T::Struct
    class B; end
  end
RUBY

Non-T::Struct works:

namespace = Parlour::TypeLoader.load_source(<<-RUBY)
  class A < Foo; end
  class A::B; end
RUBY

Improved and enforced consistency between Namespace#create_x and X#initialize

It would be good to ensure that the signatures for Namespace#create_x methods are the same as the signatures for the corresponding X#initialize methods (besides the generator parameter), improving the internal consistency of Parlour.

For example, the signature for Namespace#create_method is:

def create_method(name, parameters: nil, return_type: nil, returns: nil, abstract: false, ...)

But the signature for Method#initialize uses some positional arguments as keyword arguments instead:

def initialize(generator, name, parameters, return_type = nil, abstract: false, ...)

(This would have to be considered a breaking change.)

This consistency could also to enforced in tests by having a test which grabs method parameters and compares for each, adding a layer of protection against adding a parameter to one but not the other by accident.

Support for loading constants

Is your feature request related to a problem? Please describe.
Parlour cannot currently load constants, which means Brain Freeze can't put them in its generated RBI.

Describe the solution you'd like
Parlour should load constants from source code.

Add doc generation

Is your feature request related to a problem? Please describe.
Currently, there is no easy way to read the YARD documentation for Parlour.

Describe the solution you'd like
CI should rebuild the docs, and they should be hosted through GitHub Pages.

Describe alternatives you've considered
We could rely on a third-party docs service, but these could be slow and unreliable, and are only updated when a new gem version is published.

Additional context
discordrb does this with Travis.

Migrate to type_alias with blocks

Declaring a Sorbet type alias now requires a block, not an argument:

MyInt = T.type_alias { Integer }

Parlour should be updated to reflect this change.

Ignore sorbet/ in gemspec

I just noticed parlour has quite a few files because of its sorbet/ directory being shipped as part of the gem, we should probably ignore the directory when packaging the gem?

Create blank line

I'd like to add a blank line after a comment, so a method like add_blank_line would be nice to have :)

Maybe also add_blank_lines with the ability so specify the number?

logo proposal for parlour

Hi! @AaronC81
I'm interested in collaborating with this initiative by delivering a series of logotype options that fits this project for the best!

If interested let me know so we can get started!

Cheers!

Error 5028 for "Regex = Regex = T.let(T.unsafe(nil), Regexp)"

To Reproduce

When I ran parlour's "rake tc" command this morning, I got:

sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi:526:
Constants must have type annotations with T.let when specifying # typed: strict https://srb.help/5028
    526 |      Regex = Regex = T.let(T.unsafe(nil), Regexp)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Errors: 1

This is related to the 8/19/2019 3:16pm "discuss" post.

Parlour is broken with Ruby 2.4 & Sorbet master

Describe the bug
Build failure with Ruby 2.4 & Sorbet master. IDK if this is a sorbet bug or we need to make changes in parlour. Maybe the right fix is to file issue to Sorbet team and fix it.

To Reproduce
See https://travis-ci.com/chanzuckerberg/sorbet-rails/jobs/295030504
It seems simply requiring parlour breaks the code

Additional information
Add any other information about the problem here. Please include the version of Parlour you're using as well as the versions of any plugins.

Failure/Error: require('parlour')
NoMethodError:
  private method `define_method' called for Parlour::TypeParser::IntermediateSig:Class
  Did you mean?  redefine_method
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/props/has_lazily_specialized_methods.rb:66:in `enqueue_lazy_method_definition!'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/private/methods/call_validation.rb:126:in `call'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/private/methods/call_validation.rb:126:in `validate_call'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/private/methods/_methods.rb:238:in `block in _on_method_added'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/props/serializable.rb:168:in `add_prop_definition'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/props/decorator.rb:424:in `prop_defined'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/private/methods/_methods.rb:240:in `call'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/private/methods/_methods.rb:240:in `block in _on_method_added'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/sorbet-runtime-0.5.5417/lib/types/props/_props.rb:109:in `prop'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/parlour-2.0.0/lib/parlour/type_parser.rb:262:in `<class:IntermediateSig>'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/parlour-2.0.0/lib/parlour/type_parser.rb:261:in `<class:TypeParser>'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/parlour-2.0.0/lib/parlour/type_parser.rb:17:in `<module:Parlour>'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/parlour-2.0.0/lib/parlour/type_parser.rb:15:in `<top (required)>'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:293:in `require'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:293:in `block in require'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:259:in `load_dependency'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:293:in `require'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/parlour-2.0.0/lib/parlour.rb:31:in `<top (required)>'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:293:in `require'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:293:in `block in require'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:259:in `load_dependency'
# /home/travis/.rvm/gems/ruby-2.4.6/gems/activesupport-5.0.7.2/lib/active_support/dependencies.rb:293:in `require'
# ./lib/sorbet-rails/model_rbi_formatter.rb:2:in `<top (required)>'

Add support for T::Struct

Is your feature request related to a problem? Please describe.
Besides using arbitrary code insertion, there's currently no way to generate an RBI for a T::Struct with properties.

Describe the solution you'd like
There should be a "specialism" of the class RBI object for modelling a T::Struct. When an RBI is generated, this should produce its properties and possibly its constructor.

Describe alternatives you've considered
Arbitrary code insertion can be used, but this is very ugly.

Better Namespace traversal

Is your feature request related to a problem? Please describe.
Currently, the only way to traverse a tree of RBI objects is by using array methods on the object's child arrays. This works, but it would be nice to have some helper methods too.

Describe the solution you'd like
Namespaces should get some new methods to make traversal easier, perhaps a versatile find method taking keyword arguments, and/or a method to get a child by name.

Improve error output when plugins throw an exception

Is your feature request related to a problem? Please describe.
The message when a plugin throws an exception doesn't contain much information:

!!! This plugin threw an exception: {exception message}

Describe the solution you'd like
It would be better to have plugin exceptions contain more information, such as a stack trace and details about the plugin.

Supporting Overloads

Sorbet now supported overloaded method signatures.

Poking around parlour I do not see a way to do this yet. If we are not yet able to generate multiple sigs per method, I would like to have a chat about options. If they have not been implemented by the time I need them, I'll add them myself if we can agree on an appropriate pattern.

My first instinct is:

@node.create_method('foo', the_rest) do |method|
  method.add_signature(takes_the_same_arguments_less_the_name)
end

Option for `run_plugins` to re-raise exception

Is your feature request related to a problem? Please describe.
There should be an option for run_plugins to re-raise exception. I'm using this outside in code and it's not obvious to debug when the error is silenced.

    def self.run_plugins(plugins, generator)
      plugins.each do |plugin|
        puts "=== #{plugin.class.name}"
        generator.current_plugin = plugin
        plugin.generate(generator.root)
      rescue Exception => e
        puts "!!! This plugin threw an exception: #{e}"
      end
    end

Describe the solution you'd like

    def self.run_plugins(plugins, generator, allow_failure: true)
      plugins.each do |plugin|
        puts "=== #{plugin.class.name}"
        generator.current_plugin = plugin
        plugin.generate(generator.root)
      rescue Exception => e
        raise e unless allow_failure
        puts "!!! This plugin threw an exception: #{e}"
      end
   end

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context about the feature request here.

Specify version of sorbet-runtime in gemspec file

Describe the bug
Can we specify the version of sorbet in the gemspec file? Without the specification, every time I release a new version of sorbet-rails, it seems to try to install the newest version of sorbet & sorbet-runtime. This is not desirable because the codebases that use sorbet-rails might be using an old version of sorbet and don't want to upgrade yet.

I think the cause is this:

  spec.add_dependency "sorbet-runtime"

To Reproduce
Upgrade sorbet-rails in a repo that use a fixed version of sorbet-runtime. Observe that it also tries to upgrade sorbet-runtime's version of the repo.

Expected behavior
It shouldn't try to change the sorbet-runtime version of the repo

Actual behavior

Additional information

Add a "flat" formatting option

@manhhung741 suggested this on the original Sord RFC for this project, and I think it's a good option to have. If this option is set, then generate this style:

module A
end

class A::B
end

Rather than this:

module A
  class B
  end
end

Run typecheck in CI

We should be running srb tc in CI to make sure the typechecks don't start failing. Right now srb tc fails because the run_plugins method is missing a parameter in its sig.

Loosen dependencies on commander and rainbow

parlour (1.0.0)
   commander (~> 4.4.0)
   rainbow (~> 3.0.0)

I think it's pretty unlikely that either commander or rainbow will have significant breakage between minor versions, so limiting seems unnecessary. Could we update the gemspec to rely on ~> 4.5 and ~> 3.0 respectively?

Parser fails on undeclared block argument

Version: 2.0.0

Hi! First of all, thanks for the great project!

I ran into an interesting crash when running parlour on my codebase. When parsing, parlour crashes with Parlour::ParseError: mismatching number of arguments in sig and def if a function has a block argument in the def but not in the sig (I had some of these generated by rails_rbi).
https://github.com/AaronC81/parlour/blob/master/lib/parlour/type_parser.rb#L392

Example:

# typed: strong

class A
  extend T::Sig

  sig { params(a: String).void }
  def bar(a, &blk); end
end
> Parlour::TypeLoader.load_file('./test.rb')
Parlour::ParseError: mismatching number of arguments in sig and def

Suprisingly, this seems like valid sorbet, even though it's not mentioned in the doc.
https://sorbet.run/#%23%20typed%3A%20strong%0A%0Aclass%20A%0A%20%20extend%20T%3A%3ASig%0A%0A%20%20sig%20%7B%20params(a%3A%20String).void%20%7D%0A%20%20def%20bar(a%2C%20%26blk)%3B%20end%0Aend%0A

Don't force `# typed: strong`

Is your feature request related to a problem? Please describe.
I'm trying to recreate the Helper support in Sorbet Rails using Parlour, to see how well it'd work. So I'd like to create a file like this:

# This is an autogenerated file for Rails helpers.
# Please rerun rake rails_rbi:helpers to regenerate.
# typed: strong

module FooHelper
  include Kernel
end

Describe the solution you'd like
I'd like to be able to specify the typed level (ignore, false, true, strong, or strict), and also add comments before the typed comment if I want to.

Standardising object ordering

This was brought up briefly in #3, but I'll bring it up again here with ordering too.

Currently, when a namespace is converted into its RBI lines, the lines are generated in the following order:

  1. All includes are added, in order of add_include calls.
  2. All extends are added, in order of add_extend calls.
  3. All constants are added, in order of add_constant calls.
  4. All children are added, in the order of create_... calls. (Children are classes, modules, methods and attributes - all of the subclasses of RbiObject.)

The result of this is that the following call structure:

generator.root.create_module('M') do |m|
  m.add_constant('Const', '3')
  m.create_class('A')
  m.add_include('I')
  m.create_class('B')
end

Generates the following RBI, where the order of added objects is not preserved.

module M
  include I
  Const = 3 

  class A
  end
  
  class B
  end
end

Could this be considered an advantage, as includes, extends and constants all appear at the top where it is conventional for them to appear, or could this behaviour break certain applications?

Set up issue templates

I still need to set up issue templates for this repo - I'll probably just copy the ones from Sord.

TypeLoader#load_project failing when method has the same name as attr_*

Describe the bug
Parlour::TypeLoader.load_project fails when a class defines a method with the same name as an attr_*.

To Reproduce

namespace = Parlour::TypeLoader.load_source(<<-RUBY)
  class A
    attr_writer :foo
    def foo; end
  end
RUBY

# Called within `load_project`
Parlour::ConflictResolver.new.resolve_conflicts(namespace) do |n, o|
  raise "conflict of #{o.length} objects: #{n}"
end

Expected behavior
No conflict.

Actual behavior
Conflict.

Additional information
In the example provided it's an attr_writer mixed with a "getter" method which actually don't conflict in real life. Interestingly, attr_writer mixed with a "setter" method does not raise an exception since the ConflictResolver is only matching on names but one would override the other in real life.

i.e. this has no conflicts:

namespace = Parlour::TypeLoader.load_source(<<-RUBY)
  class A
    attr_writer :foo
    def foo=(v); end
  end
RUBY

create_includes and create_extends

Is your feature request related to a problem? Please describe.
Currently, to create a module like this:

module Foo
  include Bar
  include Baz
  include Qux
  include Quux
end

You need to write code like this:

@parlour = Parlour::RbiGenerator.new
@parlour.root.create_module(name: 'Foo') do |mod|
  mod.create_include(name: 'Bar')
  mod.create_include(name: 'Baz')
  mod.create_include(name: 'Qux')
  mod.create_include(name: 'Quux')
end

Or you can hack around the limitation with something like this:

['Bar', 'Baz', 'Qux', 'Quux'].each { |includable| mod.create_include(name: includable) }

Describe the solution you'd like

I'd like to be able to instead use a method like create_includes which takes an array, so the code looks like this:

@parlour = Parlour::RbiGenerator.new
@parlour.root.create_module(name: 'Foo') do |mod|
  mod.create_includes(['Bar', 'Baz', 'Qux', 'Quux'])
end

And ditto for extend.

Add support for sig(:final)

Is your feature request related to a problem? Please describe.
Sorbet recently added support for Final Methods/Classes/Modules with sig(:final) and final!: https://sorbet.org/docs/final

Currently there's no way for Parlour to create these.

Describe the solution you'd like
Add an optional final parameter on create_method that takes a boolean, which would default to false

@parlour.root.create_method('foo', return_type: nil, final: true)
# => sig(:final) { void }
#    def foo; end

And I guess add the same to create_class and create_module which adds final! to the class/module.

Additional context
The feature is still experimental, so we should probably hold off on implementing it until it's a bit more completed.

constants on a metaclass?

I have a gem that provides something like

class Base
  extend Enumerable
end

and I'm trying out parlour to generate RBI for subclasses of that. Sorbet gets really angry about not having Elem = type_member(somethingsomething) (even though that seems completely undocumented).

I had been fixing that by removing the extend in the gems rbi since I'm not actually using any of those methods, however I just discovered that I can also fix it by doing

class MyThing < Base
  class << self
    Elem = type_member(fixed: MyThing)
  end
end

I can't find a way to generate this from parlour though. Is it possible?

TypeParser needs a refactor

TypeParser is a complete mess of a file, and desperately needs a refactor to make it more manageable and more modular.

Accept Symbols as well as Strings for method and parameter names

Currently, parameter names and method names are

# What it looks like right now
foo.create_method('add_two_integers', [
  Parlour::RbiGenerator::Parameter.new('a', type: 'Integer'),
  Parlour::RbiGenerator::Parameter.new('b', type: 'Integer')
], 'Integer')

# What I want
foo.create_method(:add_two_integers, [
  Parlour::RbiGenerator::Parameter.new(:a, type: 'Integer'),
  Parlour::RbiGenerator::Parameter.new(:b, type: 'Integer')
], 'Integer')

The downside of this is that you lose the ability to set the parameter as a required keyword parameter (Parlour::RbiGenerator::Parameter.new('a:', type: 'Integer') can't be represented with an equivalent symbol). Nor can we do splat parameters or block parameters.

I don't want to complicate things too much, but I guess either we could add a parameter like required that can be set to true (and wouldn't be allowed with default), that'd append : to the parameter name.

foo.create_method(:add_two_integers, [
  Parlour::RbiGenerator::Parameter.new(:a, type: 'Integer', required: true)
], 'Integer')

# => sig { params(a: Integer).returns(Integer) }
#    def add_two_integers(a:); end

As for block/splat parameters, I'm not really sure. Adding more named parameters like block or splat seems like overkill.

Type mismatch when passing default values

I have test.rb:

require_relative './lib/parlour'

generator = Parlour::RbiGenerator.new
generator.root.create_module('A') do |a|
  a.create_class('Foo') do |foo|
    foo.create_method('add_two_integers', [
      Parlour::RbiGenerator::Parameter.new('a', type: 'Integer', default: '10'),
      Parlour::RbiGenerator::Parameter.new('b', type: 'String', default: '10')
    ], 'String')
  end
end

puts generator.rbi # => Our RBI as a string

and it outputs this:

module A
  class Foo
    sig { params(a: Integer, b: String).returns(String) }
    def add_two_integers(a = 10, b = 10); end
  end
end

Parlour::RbiGenerator::Parameter.new('a', type: 'Integer', default: '10') and Parlour::RbiGenerator::Parameter.new('b', type: 'String', default: '10') both generate an Integer as the default value, when b should have 10 represented as a String.

The library shouldn't allow a default that doesn't match the type of the parameter (also, allow non-string defaults, currently passing an Integer as the default causes an error).

Support for class attributes

Is your feature request related to a problem? Please describe.
Support for attr_ methods on the singleton class level is required to fully implement AaronC81/sord#58, as YARD detects these class attributes.

Describe the solution you'd like
Attributes should have an option called class_attribute, which behaves like a method's class_method option. All attributes with class_attribute: true will be generated inside a class << self section of the class. For example:

class X
  class << self
    sig { returns(String) } 
    attr_accessor :foo 
  end
end

Describe alternatives you've considered
Another way to implement this would be to allow class << self blocks to be generated anywhere, however this would make conflict resolution much more difficult.

Sort the methods by name before exporting

Is your feature request related to a problem? Please describe.
Currently, I think the method signatures are outputted in the order they are created. In certain cases, I find it useful to be able to sort the methods in the same module/object before printing them out. It'll b

Describe the solution you'd like
An option to sort methods/child modules before calling parlour.rbi. Or calling parlour.rbi(sort_options:...)

Describe alternatives you've considered
None

Additional context
I'm using Parlour to generate rbis for routes generated by rails. Rails' API may call it in any order. When there is a new route added, the whole file might be changed. If there is an option to sort the generated methods, it'll give a more consistent output.

https://github.com/chanzuckerberg/sorbet-rails/blob/master/lib/sorbet-rails/routes_rbi_formatter.rb#L51

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.