parlour 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2e368fa34eb2073d6b20beda3455dbf51db175ec2f5ef6c085de62f62601888
4
- data.tar.gz: c0c0b1e1f0e9df83e76cba72609083058149d7de169afcb07418a3117b00880a
3
+ metadata.gz: 88487f0d150090c2b0877343e66efc37d0643892fd3b99966f0d4cca992d29e1
4
+ data.tar.gz: eb2f33d58f3606186d9691845d50071d456107d9a913947f70b89cfa2c9a6492
5
5
  SHA512:
6
- metadata.gz: e6d87997cabce942aa838efc114071f08465de8fb7c442eb58a6bce45d1e95bb78369183fa11568da31b52038bbb38af9ddf78617f5328536bbd6f3fbd0976f7
7
- data.tar.gz: 70b90d57fc92f67934816152c6ac40505e2305400cbadab4313d3e01b079a0637e6e056c1f58a93db455a6d5d715d58c6cdc8e4b43f9274fa23d718f225519fa
6
+ metadata.gz: 8ae0f4def7984b641df8b45000eddbafcf10048ccc772f4802d413e79b8e1431afc58963f2c1e40533b99729dad4801c3f328fc3a6f35cb654ad9c7b092a493c
7
+ data.tar.gz: ad6f1e9bd34f9b56aaa4ae1eb8ac30cded890f2b5f64672d9bdc5f3ca82d18ec4ee2b55cea1c7abd65ba32cd03d874715d470f4cd72c2a733d2ba209a7b06755
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create an issue to report a bug
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Include the code (and if applicable your `parlour` CLI invocation) where Parlour is doing something wrong.
15
+
16
+ **Expected behavior**
17
+ A clear and concise description of what you expected to happen. It may be useful to write your own expected RBI for your code.
18
+
19
+ **Actual behavior**
20
+ A description of what actually happened. Please post any incorrect RBI signatures, as well as the command-line output of Parlour.
21
+
22
+ **Additional information**
23
+ 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.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context about the feature request here.
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ before_install:
3
+ - gem install bundler
4
+ rvm:
5
+ - 2.6
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Parlour
2
2
 
3
- Parlour is an RBI generator and merger for Sorbet. Once done, it'll consist of
4
- two parts:
3
+ [![Build Status](https://travis-ci.org/AaronC81/parlour.svg?branch=master)](https://travis-ci.org/AaronC81/parlour)
4
+
5
+ Parlour is an RBI generator and merger for Sorbet. It consists of two key parts:
5
6
 
6
7
  - The generator, which outputs beautifully formatted RBI files, created using
7
8
  an intuitive DSL.
@@ -12,6 +13,11 @@ two parts:
12
13
 
13
14
  ## Usage
14
15
 
16
+ There aren't really any docs currently, so have a look around the code to find
17
+ any extra options you may need.
18
+
19
+ ### Using just the generator
20
+
15
21
  Here's a quick example of how you might generate an RBI currently, though this
16
22
  API is very likely to change:
17
23
 
@@ -47,8 +53,54 @@ module A
47
53
  end
48
54
  ```
49
55
 
50
- There aren't really any docs currently, so have a look around the code to find
51
- any extra options you may need.
56
+ ### Writing a plugin
57
+ Plugins are better than using the generator alone, as your plugin can be
58
+ combined with others to produce larger RBIs without conflicts.
59
+
60
+ We could write the above example as a plugin like this:
61
+
62
+ ```ruby
63
+ require 'parlour'
64
+
65
+ class MyPlugin < Parlour::Plugin
66
+ def generate(root)
67
+ root.create_module('A') do |a|
68
+ a.create_class('Foo') do |foo|
69
+ foo.create_method('add_two_integers', [
70
+ Parlour::RbiGenerator::Parameter.new('a', type: 'Integer'),
71
+ Parlour::RbiGenerator::Parameter.new('b', type: 'Integer')
72
+ ], 'Integer')
73
+ end
74
+
75
+ a.create_class('Bar', superclass: 'Foo')
76
+ end
77
+ end
78
+ end
79
+ ```
80
+
81
+ (Obviously, your plugin will probably examine a codebase somehow, to be more
82
+ useful!)
83
+
84
+ You can then run several plugins, combining their output and saving it into one
85
+ RBI file, using the command-line tool. For example, if that code was in a file
86
+ called `plugin.rb`, this would save the RBI into `output.rbi`:
87
+
88
+ ```
89
+ parlour --relative-requires plugin.rb MyPlugin output.rbi
90
+ ```
91
+
92
+ You can also use plugins from gems. If that plugin was published as a gem called
93
+ `parlour-gem`:
94
+
95
+ ```
96
+ parlour --requires parlour-gem MyPlugin output.rbi
97
+ ```
98
+
99
+ The real power of this is the ability to use many plugins at once:
100
+
101
+ ```
102
+ parlour --requires gem1,gem2,gem3 Gem1::Plugin Gem2::Plugin Gem3::Plugin output.rbi
103
+ ```
52
104
 
53
105
  ## Code Structure
54
106
 
@@ -80,16 +132,23 @@ any extra options you may need.
80
132
 
81
133
  ### Generation
82
134
  Everything that can generate lines of the RBI implements the
83
- `RbiGenerator::RbiObject` interface. This defines one function, `generate_rbi`,
84
- which accepts the current indentation level and a set of formatting options.
135
+ `RbiGenerator::RbiObject` abstract class. The function `generate_rbi`
136
+ accepts the current indentation level and a set of formatting options.
85
137
  (Each object is responsible for generating its own indentation; that is, the
86
138
  lines generated by a child object should not then be indented by its parent.)
87
139
 
88
140
  I think generation is quite close to done, but it still needs features like
89
141
  constants and type parameters.
90
142
 
143
+ ### Plugins
144
+ Plugins are automatically detected when they subclass `Plugin`. Plugins can then
145
+ be run by passing an array of them, along with an `RbiGenerator`, to
146
+ `Plugin.run_plugins`. Once done, the generator will be populated with a tree of
147
+ (possibly conflicting) `RbiObjects` - the conflict resolver (detailed below)
148
+ will clear up any conflicts.
149
+
91
150
  ### Conflict Resolution
92
- This will be a key part of the plugin/build system. The `ConflictResolver` takes
151
+ This is a key part of the plugin/build system. The `ConflictResolver` takes
93
152
  a namespace from the `RbiGenerator` and merges duplicate items in it together.
94
153
  This means that many plugins can generate their own signatures which are all
95
154
  bundled into one, conflict-free output RBI.
@@ -0,0 +1,6 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ task :default => [:spec]
4
+
5
+ desc "Run the specs."
6
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+ require 'parlour'
3
+ require 'commander/import'
4
+ require 'bundler'
5
+ require 'rainbow'
6
+
7
+ program :name, 'parlour'
8
+ program :version, Parlour::VERSION
9
+ program :description, 'An RBI generator and plugin system'
10
+
11
+ default_command :run
12
+ command :run do |c|
13
+ c.syntax = 'parlour run <plugins...> <output-file> [options]'
14
+ c.description = 'Generates an RBI file from a list of plugins'
15
+ c.option '--requires STRING', String, 'A comma-separated string of gems to require'
16
+ c.option '--relative-requires STRING', String, 'A comma-separated string of files to require, relative to the working dir'
17
+ c.option '--tab-size INTEGER', Integer, 'The size of tabs to use'
18
+ c.option '--break-params INTEGER', Integer, 'Break params onto their own lines if there are this many'
19
+
20
+ c.action do |args, options|
21
+ options.default(
22
+ tab_size: 2,
23
+ break_params: 4,
24
+ requires: '',
25
+ relative_requires: ''
26
+ )
27
+
28
+ options.requires.split(',').each { |source| require(source) }
29
+ options.relative_requires.split(',').each do |source|
30
+ require(File.join(Dir.pwd, source))
31
+ end
32
+
33
+ *plugin_names, output_file = args
34
+
35
+ raise 'no output file specified' if output_file.nil?
36
+
37
+ plugin_instances = []
38
+
39
+ # Collect the instances of each plugin into an array
40
+ plugin_names.each do |name|
41
+ plugin = Parlour::Plugin.registered_plugins[name]
42
+ raise "missing plugin #{name}" unless plugin
43
+ plugin_instances << plugin
44
+ end
45
+
46
+ # Create a generator instance and run all plugins on it
47
+ gen = Parlour::RbiGenerator.new(
48
+ break_params: options.break_params,
49
+ tab_size: options.tab_size
50
+ )
51
+ Parlour::Plugin.run_plugins(plugin_instances, gen)
52
+
53
+ # Run a pass of the conflict resolver
54
+ Parlour::ConflictResolver.new.resolve_conflicts(gen.root) do |msg, candidates|
55
+ puts Rainbow('Conflict! ').red.bright.bold + Rainbow(msg).blue.bright
56
+ puts 'Multiple different definitions have been produced for the same object.'
57
+ puts 'They could not be merged automatically.'
58
+ puts Rainbow('What would you like to do?').bold + ' Type a choice and press Enter.'
59
+ puts
60
+ puts Rainbow(' [0] ').yellow + 'Remove ALL definitions'
61
+ puts
62
+ puts "Or select one definition to keep:"
63
+ puts
64
+ candidates.each.with_index do |candidate, i|
65
+ puts Rainbow(" [#{i + 1}] ").yellow + candidate.describe
66
+ end
67
+ puts
68
+ choice = ask("? ", Integer) { |q| q.in = 0..candidates.length }
69
+ choice == 0 ? nil : candidates[choice - 1]
70
+ end
71
+
72
+ # Write the final RBI
73
+ File.write(output_file, gen.rbi)
74
+ end
75
+ end
@@ -3,9 +3,12 @@ require 'sorbet-runtime'
3
3
 
4
4
  require 'parlour/version'
5
5
 
6
+ require 'parlour/plugin'
7
+
6
8
  require 'parlour/rbi_generator/parameter'
7
9
  require 'parlour/rbi_generator/rbi_object'
8
10
  require 'parlour/rbi_generator/method'
11
+ require 'parlour/rbi_generator/attribute'
9
12
  require 'parlour/rbi_generator/options'
10
13
  require 'parlour/rbi_generator/namespace'
11
14
  require 'parlour/rbi_generator/module_namespace'
@@ -1,5 +1,7 @@
1
1
  # typed: true
2
2
  module Parlour
3
+ # Responsible for resolving conflicts (that is, multiple definitions with the
4
+ # same name) between objects defined in the same namespace.
3
5
  class ConflictResolver
4
6
  extend T::Sig
5
7
 
@@ -12,17 +14,33 @@ module Parlour
12
14
  ).returns(RbiGenerator::RbiObject)
13
15
  ).void
14
16
  end
17
+ # Given a namespace, attempts to automatically resolve conflicts in the
18
+ # namespace's definitions. (A conflict occurs when multiple objects share
19
+ # the same name.)
20
+ #
21
+ # All children of the given namespace which are also namespaces are
22
+ # processed recursively, so passing {RbiGenerator#root} will eliminate all
23
+ # conflicts in the entire object tree.
24
+ #
25
+ # If automatic resolution is not possible, the block passed to this method
26
+ # is invoked and passed two arguments: a message on what the conflict is,
27
+ # and an array of candidate objects. The block should return one of these
28
+ # candidate objects, which will be kept, and all other definitions are
29
+ # deleted. Alternatively, the block may return nil, which will delete all
30
+ # definitions. The block may be invoked many times from one call to
31
+ # {resolve_conflicts}, one for each unresolvable conflict.
32
+ #
33
+ # @param namespace [RbiGenerator::Namespace] The starting namespace to
34
+ # resolve conflicts in.
35
+ # @yieldparam message [String] A descriptional message on what the conflict is.
36
+ # @yieldparam candidates [Array<RbiGenerator::RbiObject>] The objects for
37
+ # which there is a conflict.
38
+ # @yieldreturn [RbiGenerator::RbiObject] One of the +candidates+, which
39
+ # will be kept, or nil to keep none of them.
40
+ # @return [void]
15
41
  def resolve_conflicts(namespace, &resolver)
16
42
  # Check for multiple definitions with the same name
17
- grouped_by_name_children = namespace.children.group_by do |rbi_obj|
18
- if RbiGenerator::ModuleNamespace === rbi_obj \
19
- || RbiGenerator::ClassNamespace === rbi_obj \
20
- || RbiGenerator::Method === rbi_obj
21
- rbi_obj.name
22
- else
23
- raise "unsupported child of type #{T.cast(rbi_obj, Object).class}"
24
- end
25
- end
43
+ grouped_by_name_children = namespace.children.group_by(&:name)
26
44
 
27
45
  grouped_by_name_children.each do |name, children|
28
46
  if children.length > 1
@@ -43,12 +61,6 @@ module Parlour
43
61
  next
44
62
  end
45
63
 
46
- # Are all of the children equivalent? If so, just keep one of them
47
- if all_eql?(children)
48
- namespace.children << T.must(children.first)
49
- next
50
- end
51
-
52
64
  # Can the children merge themselves automatically? If so, let them
53
65
  first, *rest = children
54
66
  first, rest = T.must(first), T.must(rest)
@@ -64,18 +76,33 @@ module Parlour
64
76
  end
65
77
  end
66
78
 
67
- # TODO: recurse to deeper namespaces
79
+ # Recurse to child namespaces
80
+ namespace.children.each do |child|
81
+ resolve_conflicts(child, &resolver) if RbiGenerator::Namespace === child
82
+ end
68
83
  end
69
84
 
85
+ private
86
+
70
87
  sig { params(arr: T::Array[T.untyped]).returns(T.nilable(Class)) }
88
+ # Given an array, if all elements in the array are instances of the exact
89
+ # same class, returns that class. If they are not, returns nil.
90
+ #
91
+ # @param arr [Array] The array.
92
+ # @return [Class, nil] Either a class, or nil.
71
93
  def single_type_of_array(arr)
72
- array_types = arr.map { |c| T.cast(c, Object).class }.uniq
94
+ array_types = arr.map { |c| c.class }.uniq
73
95
  array_types.length == 1 ? array_types.first : nil
74
96
  end
75
97
 
76
98
  sig { params(arr: T::Array[T.untyped]).returns(T::Boolean) }
99
+ # Given an array, returns true if all elements in the array are equal by
100
+ # +==+. (Assumes a transitive definition of +==+.)
101
+ #
102
+ # @param arr [Array] The array.
103
+ # @return [Boolean] A boolean indicating if all elements are equal by +==+.
77
104
  def all_eql?(arr)
78
105
  arr.each_cons(2).all? { |x, y| x == y }
79
106
  end
80
107
  end
81
- end
108
+ end
@@ -0,0 +1,53 @@
1
+ # typed: true
2
+ module Parlour
3
+ # The base class for user-defined RBI generation plugins.
4
+ # @abstract
5
+ class Plugin
6
+ extend T::Sig
7
+ extend T::Helpers
8
+ abstract!
9
+
10
+ @@registered_plugins = {}
11
+
12
+ sig { returns(T::Hash[String, Plugin]) }
13
+ # Returns all registered plugins, as a hash of their paths to the {Plugin}
14
+ # instances themselves.
15
+ #
16
+ # @return [{String, Plugin}]
17
+ def self.registered_plugins
18
+ @@registered_plugins
19
+ end
20
+
21
+ sig { params(new_plugin: T.class_of(Plugin)).void }
22
+ # Called automatically by the Ruby interpreter when {Plugin} is subclassed.
23
+ # This registers the new subclass into {registered_plugins}.
24
+ #
25
+ # @param new_plugin [Plugin] The new plugin.
26
+ # @return [void]
27
+ def self.inherited(new_plugin)
28
+ registered_plugins[T.must(new_plugin.name)] = new_plugin.new
29
+ end
30
+
31
+ sig { params(plugins: T::Array[Plugin], generator: RbiGenerator).void }
32
+ # Runs an array of plugins on a given generator instance.
33
+ #
34
+ # @param plugins [Array<Plugin>] An array of {Plugin} instances.
35
+ # @param generator [RbiGenerator] The {RbiGenerator} to run the plugins on.
36
+ # @return [void]
37
+ def self.run_plugins(plugins, generator)
38
+ plugins.each do |plugin|
39
+ generator.current_plugin = plugin
40
+ plugin.generate(generator.root)
41
+ end
42
+ end
43
+
44
+ sig { abstract.params(root: RbiGenerator::Namespace).void }
45
+ # Plugin subclasses should redefine this method and do their RBI generation
46
+ # inside it.
47
+ #
48
+ # @abstract
49
+ # @param root [RbiGenerator::Namespace] The root {RbiGenerator::Namespace}.
50
+ # @return [void]
51
+ def generate(root); end
52
+ end
53
+ end
@@ -1,23 +1,49 @@
1
1
  # typed: true
2
2
  module Parlour
3
+ # The RBI generator.
3
4
  class RbiGenerator
4
5
  extend T::Sig
5
6
 
6
7
  sig { params(break_params: Integer, tab_size: Integer).void }
8
+ # Creates a new RBI generator.
9
+ #
10
+ # @example Create a default generator.
11
+ # generator = Parlour::RbiGenerator.new
12
+ #
13
+ # @example Create a generator with a custom +tab_size+ of 3.
14
+ # generator = Parlour::RbiGenerator.new(tab_size: 3)
15
+ #
16
+ # @param break_params [Integer] If there are at least this many parameters in a
17
+ # Sorbet +sig+, then it is broken onto separate lines.
18
+ # @param tab_size [Integer] The number of spaces to use per indent.
19
+ # @return [void]
7
20
  def initialize(break_params: 4, tab_size: 2)
8
21
  @options = Options.new(break_params: break_params, tab_size: tab_size)
9
- @root = Namespace.new
22
+ @root = Namespace.new(self)
10
23
  end
11
24
 
12
25
  sig { returns(Options) }
26
+ # The formatting options for this generator.
27
+ # @return [Options]
13
28
  attr_reader :options
14
29
 
15
30
  sig { returns(Namespace) }
31
+ # The root {Namespace} of this generator.
32
+ # @return [Namespace]
16
33
  attr_reader :root
17
34
 
35
+ sig { returns(T.nilable(Plugin)) }
36
+ # The plugin which is currently generating new definitions.
37
+ # {Plugin#run_plugins} controls this value.
38
+ # @return [Plugin, nil]
39
+ attr_accessor :current_plugin
40
+
18
41
  sig { returns(String) }
42
+ # Returns the complete contents of the generated RBI file as a string.
43
+ #
44
+ # @return [String] The generated RBI file
19
45
  def rbi
20
46
  root.generate_rbi(0, options).join("\n")
21
47
  end
22
48
  end
23
- end
49
+ end