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 +4 -4
- data/.github/ISSUE_TEMPLATE/bug-report.md +23 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +20 -0
- data/.travis.yml +5 -0
- data/README.md +66 -7
- data/Rakefile +6 -0
- data/exe/parlour +75 -0
- data/lib/parlour.rb +3 -0
- data/lib/parlour/conflict_resolver.rb +45 -18
- data/lib/parlour/plugin.rb +53 -0
- data/lib/parlour/rbi_generator.rb +28 -2
- data/lib/parlour/rbi_generator/attribute.rb +68 -0
- data/lib/parlour/rbi_generator/class_namespace.rb +49 -13
- data/lib/parlour/rbi_generator/method.rb +123 -31
- data/lib/parlour/rbi_generator/module_namespace.rb +43 -14
- data/lib/parlour/rbi_generator/namespace.rb +264 -32
- data/lib/parlour/rbi_generator/options.rb +37 -1
- data/lib/parlour/rbi_generator/parameter.rb +52 -1
- data/lib/parlour/rbi_generator/rbi_object.rb +116 -3
- data/lib/parlour/version.rb +2 -1
- data/parlour.gemspec +1 -0
- data/plugin_examples/foobar_plugin.rb +13 -0
- metadata +26 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88487f0d150090c2b0877343e66efc37d0643892fd3b99966f0d4cca992d29e1
|
4
|
+
data.tar.gz: eb2f33d58f3606186d9691845d50071d456107d9a913947f70b89cfa2c9a6492
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# Parlour
|
2
2
|
|
3
|
-
|
4
|
-
|
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
|
-
|
51
|
-
|
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`
|
84
|
-
|
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
|
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.
|
data/Rakefile
ADDED
data/exe/parlour
ADDED
@@ -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
|
data/lib/parlour.rb
CHANGED
@@ -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
|
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
|
-
#
|
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|
|
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
|