parlour 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 761bffbc56ef9da0ef4c5fd4bce6ddb60dacafa0162a367fc2073d8466e37db9
4
- data.tar.gz: cb5a6966088858d39c6942d2d38ed7ff947cff26853828200eaca7313cdaca33
3
+ metadata.gz: c2e368fa34eb2073d6b20beda3455dbf51db175ec2f5ef6c085de62f62601888
4
+ data.tar.gz: c0c0b1e1f0e9df83e76cba72609083058149d7de169afcb07418a3117b00880a
5
5
  SHA512:
6
- metadata.gz: 942a3f4b6ebffed1c09ee2b396ec0230663c12f4549a7dd62ce4f72f12705976edb4a9b370aa6da806c8c40e62997c653ab057a6ac4d3aebd747f9c9b040dfc3
7
- data.tar.gz: b9327692a59d57413a6095e509b99ea55ab05629406aff1c7bd244e1a8d7fee3547fc2b3feec99556ea6d93b8f931d6f18a621283c8f813423a93bc290099fb4
6
+ metadata.gz: e6d87997cabce942aa838efc114071f08465de8fb7c442eb58a6bce45d1e95bb78369183fa11568da31b52038bbb38af9ddf78617f5328536bbd6f3fbd0976f7
7
+ data.tar.gz: 70b90d57fc92f67934816152c6ac40505e2305400cbadab4313d3e01b079a0637e6e056c1f58a93db455a6d5d715d58c6cdc8e4b43f9274fa23d718f225519fa
data/.gitignore CHANGED
@@ -6,6 +6,8 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
10
+ /.vscode
9
11
 
10
12
  # rspec failure tracking
11
13
  .rspec_status
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+
6
+ ## [0.1.1] - 2019-07-05
7
+ ### Added
8
+ - Initial release!
9
+
10
+ _(0.1.0 was a blank gem.)_
data/README.md CHANGED
@@ -1,38 +1,118 @@
1
1
  # Parlour
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/parlour`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Parlour is an RBI generator and merger for Sorbet. Once done, it'll consist of
4
+ two parts:
4
5
 
5
- TODO: Delete this and the text above, and describe your gem
6
+ - The generator, which outputs beautifully formatted RBI files, created using
7
+ an intuitive DSL.
6
8
 
7
- ## Installation
9
+ - The plugin/build system, which allows multiple Parlour plugins to generate
10
+ RBIs for the same codebase. These are combined automatically as much as
11
+ possible, but any other conflicts can be resolved manually through prompts.
8
12
 
9
- Add this line to your application's Gemfile:
13
+ ## Usage
14
+
15
+ Here's a quick example of how you might generate an RBI currently, though this
16
+ API is very likely to change:
10
17
 
11
18
  ```ruby
12
- gem 'parlour'
13
- ```
19
+ require 'parlour'
14
20
 
15
- And then execute:
21
+ generator = Parlour::RbiGenerator.new
22
+ generator.root.create_module('A') do |a|
23
+ a.create_class('Foo') do |foo|
24
+ foo.create_method('add_two_integers', [
25
+ Parlour::RbiGenerator::Parameter.new('a', type: 'Integer'),
26
+ Parlour::RbiGenerator::Parameter.new('b', type: 'Integer')
27
+ ], 'Integer')
28
+ end
16
29
 
17
- $ bundle
30
+ a.create_class('Bar', superclass: 'Foo')
31
+ end
18
32
 
19
- Or install it yourself as:
33
+ generator.rbi # => Our RBI as a string
34
+ ```
20
35
 
21
- $ gem install parlour
36
+ This will generate the following RBI:
22
37
 
23
- ## Usage
38
+ ```ruby
39
+ module A
40
+ class Foo
41
+ sig { params(a: Integer, b: Integer).returns(Integer) }
42
+ def add_two_integers(a, b); end
43
+ end
44
+
45
+ class Bar < Foo
46
+ end
47
+ end
48
+ ```
24
49
 
25
- TODO: Write usage instructions here
50
+ There aren't really any docs currently, so have a look around the code to find
51
+ any extra options you may need.
26
52
 
27
- ## Development
53
+ ## Code Structure
28
54
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
55
+ ### Overall Flow
56
+ ```
57
+ STEP 1
58
+ All plugins mutate the
59
+ instance of RbiGenerator
60
+ They generate a tree
61
+ structure of RbiObjects
62
+
63
+ +--------+ +--------+
64
+ |Plugin 1| |Plugin 2|
65
+ +----+---+ +----+---+ STEP 2
66
+ ^ ^ ConflictResolver
67
+ | | mutates the structure
68
+ +-------------------+ | | to fix conflicts
69
+ | | | |
70
+ | One instance of +-------------+ +----------------+
71
+ | RbiGenerator +--------------------->+ConflictResolver|
72
+ | | +----------------+
73
+ +---------+---------+
74
+ |
75
+ |
76
+ | +-------+ STEP 3
77
+ +--------->+ File | The final RBI is written
78
+ +-------+ to a file
79
+ ```
30
80
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
81
+ ### Generation
82
+ 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.
85
+ (Each object is responsible for generating its own indentation; that is, the
86
+ lines generated by a child object should not then be indented by its parent.)
87
+
88
+ I think generation is quite close to done, but it still needs features like
89
+ constants and type parameters.
90
+
91
+ ### Conflict Resolution
92
+ This will be a key part of the plugin/build system. The `ConflictResolver` takes
93
+ a namespace from the `RbiGenerator` and merges duplicate items in it together.
94
+ This means that many plugins can generate their own signatures which are all
95
+ bundled into one, conflict-free output RBI.
96
+
97
+ It is able to do the following merges automatically:
98
+
99
+ - If many methods are identical, delete all but one.
100
+ - If many classes are defined with the same name, merge their methods,
101
+ includes and extends. (But only if they are all abstract or all not,
102
+ and only if they don't define more than one superclass together.)
103
+ - If many modules are defined with the same name, merge their methods,
104
+ includes and extends. (But only if they are all interfaces or all not.)
105
+
106
+ If a merge can't be performed automatically, then the `#resolve_conflicts`
107
+ method takes a block. This block is passed all the conflicting objects, and one
108
+ should be selected and returned - all others will be deleted. (Alternatively,
109
+ the block can return nil, and all will be deleted.) This will allow a CLI to
110
+ prompt the user asking them what they'd like to do, in the case of conflicts
111
+ between each plugin's signatures which can't automatically be resolved.
32
112
 
33
113
  ## Contributing
34
114
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/parlour. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
115
+ 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](http://contributor-covenant.org) code of conduct.
36
116
 
37
117
  ## License
38
118
 
@@ -40,4 +120,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
120
 
41
121
  ## Code of Conduct
42
122
 
43
- Everyone interacting in the Parlour project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/parlour/blob/master/CODE_OF_CONDUCT.md).
123
+ Everyone interacting in the Parlour project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/AaronC81/parlour/blob/master/CODE_OF_CONDUCT.md).
@@ -1,6 +1,16 @@
1
- require "parlour/version"
1
+ # typed: strong
2
+ require 'sorbet-runtime'
3
+
4
+ require 'parlour/version'
5
+
6
+ require 'parlour/rbi_generator/parameter'
7
+ require 'parlour/rbi_generator/rbi_object'
8
+ require 'parlour/rbi_generator/method'
9
+ require 'parlour/rbi_generator/options'
10
+ require 'parlour/rbi_generator/namespace'
11
+ require 'parlour/rbi_generator/module_namespace'
12
+ require 'parlour/rbi_generator/class_namespace'
13
+ require 'parlour/rbi_generator'
14
+
15
+ require 'parlour/conflict_resolver'
2
16
 
3
- module Parlour
4
- class Error < StandardError; end
5
- # Your code goes here...
6
- end
@@ -0,0 +1,81 @@
1
+ # typed: true
2
+ module Parlour
3
+ class ConflictResolver
4
+ extend T::Sig
5
+
6
+ sig do
7
+ params(
8
+ namespace: RbiGenerator::Namespace,
9
+ resolver: T.proc.params(
10
+ desc: String,
11
+ choices: T::Array[RbiGenerator::RbiObject]
12
+ ).returns(RbiGenerator::RbiObject)
13
+ ).void
14
+ end
15
+ def resolve_conflicts(namespace, &resolver)
16
+ # 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
26
+
27
+ grouped_by_name_children.each do |name, children|
28
+ if children.length > 1
29
+ # We found a conflict!
30
+ # Start by removing all the conflicting items
31
+ children.each do |c|
32
+ namespace.children.delete(c)
33
+ end
34
+
35
+ # We can only try to resolve automatically if they're all the same
36
+ # type of object, so check that first
37
+ children_type = single_type_of_array(children)
38
+ unless children_type
39
+ # The types aren't the same, so ask the resovler what to do, and
40
+ # insert that (if not nil)
41
+ choice = resolver.call("Different kinds of definition for the same name", children)
42
+ namespace.children << choice if choice
43
+ next
44
+ end
45
+
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
+ # Can the children merge themselves automatically? If so, let them
53
+ first, *rest = children
54
+ first, rest = T.must(first), T.must(rest)
55
+ if T.must(first).mergeable?(T.must(rest))
56
+ first.merge_into_self(rest)
57
+ namespace.children << first
58
+ next
59
+ end
60
+
61
+ # I give up! Let it be resolved manually somehow
62
+ choice = resolver.call("Can't automatically resolve", children)
63
+ namespace.children << choice if choice
64
+ end
65
+ end
66
+
67
+ # TODO: recurse to deeper namespaces
68
+ end
69
+
70
+ sig { params(arr: T::Array[T.untyped]).returns(T.nilable(Class)) }
71
+ def single_type_of_array(arr)
72
+ array_types = arr.map { |c| T.cast(c, Object).class }.uniq
73
+ array_types.length == 1 ? array_types.first : nil
74
+ end
75
+
76
+ sig { params(arr: T::Array[T.untyped]).returns(T::Boolean) }
77
+ def all_eql?(arr)
78
+ arr.each_cons(2).all? { |x, y| x == y }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbiGenerator
4
+ extend T::Sig
5
+
6
+ sig { params(break_params: Integer, tab_size: Integer).void }
7
+ def initialize(break_params: 4, tab_size: 2)
8
+ @options = Options.new(break_params: break_params, tab_size: tab_size)
9
+ @root = Namespace.new
10
+ end
11
+
12
+ sig { returns(Options) }
13
+ attr_reader :options
14
+
15
+ sig { returns(Namespace) }
16
+ attr_reader :root
17
+
18
+ sig { returns(String) }
19
+ def rbi
20
+ root.generate_rbi(0, options).join("\n")
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,80 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbiGenerator
4
+ class ClassNamespace < Namespace
5
+ extend T::Sig
6
+
7
+ sig do
8
+ params(
9
+ name: String,
10
+ superclass: T.nilable(String),
11
+ abstract: T::Boolean,
12
+ block: T.nilable(T.proc.params(x: ClassNamespace).void)
13
+ ).void
14
+ end
15
+ def initialize(name, superclass, abstract, &block)
16
+ super(&block)
17
+ @name = name
18
+ @superclass = superclass
19
+ @abstract = abstract
20
+ end
21
+
22
+ sig do
23
+ override.params(
24
+ indent_level: Integer,
25
+ options: Options
26
+ ).returns(T::Array[String])
27
+ end
28
+ def generate_rbi(indent_level, options)
29
+ class_definition = superclass.nil? \
30
+ ? "class #{name}"
31
+ : "class #{name} < #{superclass}"
32
+
33
+ lines = []
34
+ lines << options.indented(indent_level, class_definition)
35
+ lines += [options.indented(indent_level + 1, "abstract!"), ""] if abstract
36
+ lines += super(indent_level + 1, options)
37
+ lines << options.indented(indent_level, "end")
38
+ end
39
+
40
+ sig { returns(String) }
41
+ attr_reader :name
42
+
43
+ sig { returns(T.nilable(String)) }
44
+ attr_reader :superclass
45
+
46
+ sig { returns(T::Boolean) }
47
+ attr_reader :abstract
48
+
49
+ sig do
50
+ override.params(
51
+ others: T::Array[RbiGenerator::RbiObject]
52
+ ).returns(T::Boolean)
53
+ end
54
+ def mergeable?(others)
55
+ others = T.cast(others, T::Array[ClassNamespace]) rescue (return false)
56
+ all = others + [self]
57
+
58
+ all.map(&:abstract).uniq.length == 1 &&
59
+ all.map(&:superclass).compact.uniq.length <= 1
60
+ end
61
+
62
+ sig do
63
+ override.params(
64
+ others: T::Array[RbiGenerator::RbiObject]
65
+ ).void
66
+ end
67
+ def merge_into_self(others)
68
+ others.each do |other|
69
+ other = T.cast(other, ClassNamespace)
70
+
71
+ other.children.each { |c| children << c }
72
+ other.extends.each { |e| extends << e }
73
+ other.includes.each { |i| includes << i }
74
+
75
+ @superclass = other.superclass unless superclass
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,142 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbiGenerator
4
+ class Method
5
+ extend T::Sig
6
+
7
+ include RbiObject
8
+
9
+ sig do
10
+ params(
11
+ name: String,
12
+ parameters: T::Array[Parameter],
13
+ return_type: T.nilable(String),
14
+ abstract: T::Boolean,
15
+ implementation: T::Boolean,
16
+ override: T::Boolean,
17
+ overridable: T::Boolean,
18
+ class_method: T::Boolean
19
+ ).void
20
+ end
21
+ def initialize(name, parameters, return_type = nil, abstract: false, implementation: false, override: false, overridable: false, class_method: false)
22
+ @name = name
23
+ @parameters = parameters
24
+ @return_type = return_type
25
+ @abstract = abstract
26
+ @implementation = implementation
27
+ @override = override
28
+ @overridable = overridable
29
+ @class_method = class_method
30
+ end
31
+
32
+ sig { params(other: Object).returns(T::Boolean) }
33
+ def ==(other)
34
+ Method === other &&
35
+ name == other.name &&
36
+ parameters == other.parameters &&
37
+ return_type == other.return_type &&
38
+ abstract == other.abstract &&
39
+ implementation == other.implementation &&
40
+ override == other.override &&
41
+ overridable == other.overridable &&
42
+ class_method == other.class_method
43
+ end
44
+
45
+ sig { returns(String) }
46
+ attr_reader :name
47
+
48
+ sig { returns(T::Array[Parameter]) }
49
+ attr_reader :parameters
50
+
51
+ sig { returns(T.nilable(String)) }
52
+ attr_reader :return_type
53
+
54
+ sig { returns(T::Boolean) }
55
+ attr_reader :abstract
56
+
57
+ sig { returns(T::Boolean) }
58
+ attr_reader :implementation
59
+
60
+ sig { returns(T::Boolean) }
61
+ attr_reader :override
62
+
63
+ sig { returns(T::Boolean) }
64
+ attr_reader :overridable
65
+
66
+ sig { returns(T::Boolean) }
67
+ attr_reader :class_method
68
+
69
+ sig do
70
+ implementation.params(
71
+ indent_level: Integer,
72
+ options: Options
73
+ ).returns(T::Array[String])
74
+ end
75
+ def generate_rbi(indent_level, options)
76
+ return_call = return_type ? "returns(#{return_type})" : 'void'
77
+
78
+ sig_params = parameters.map(&:to_sig_param)
79
+ sig_lines = parameters.length >= options.break_params \
80
+ ? [
81
+ options.indented(indent_level, 'sig do'),
82
+ options.indented(indent_level + 1, "#{qualifiers}params("),
83
+ ] +
84
+ (
85
+ parameters.empty? ? [] : sig_params.map do |x|
86
+ options.indented(indent_level + 2, "#{x},")
87
+ end
88
+ ) +
89
+ [
90
+ options.indented(indent_level + 1, ").#{return_call}"),
91
+ options.indented(indent_level, 'end')
92
+ ]
93
+
94
+ : [options.indented(
95
+ indent_level,
96
+ "sig { #{qualifiers}#{
97
+ parameters.empty? ? '' : "params(#{sig_params.join(', ')})"
98
+ }#{
99
+ qualifiers.empty? && parameters.empty? ? '' : '.'
100
+ }#{return_call} }"
101
+ )]
102
+
103
+ def_params = parameters.map(&:to_def_param)
104
+ name_prefix = class_method ? 'self.' : ''
105
+ def_line = options.indented(
106
+ indent_level,
107
+ "def #{name_prefix}#{name}(#{def_params.join(', ')}); end"
108
+ )
109
+
110
+ sig_lines + [def_line]
111
+ end
112
+
113
+ sig { returns(String) }
114
+ def qualifiers
115
+ result = ''
116
+ result += 'abstract.' if abstract
117
+ result += 'implementation.' if implementation
118
+ result += 'override.' if override
119
+ result += 'overridable.' if overridable
120
+ result
121
+ end
122
+
123
+ sig do
124
+ implementation.params(
125
+ others: T::Array[RbiGenerator::RbiObject]
126
+ ).returns(T::Boolean)
127
+ end
128
+ def mergeable?(others)
129
+ false
130
+ end
131
+
132
+ sig do
133
+ implementation.params(
134
+ others: T::Array[RbiGenerator::RbiObject]
135
+ ).void
136
+ end
137
+ def merge_into_self(others)
138
+ raise 'methods can never be merged like this'
139
+ end
140
+ end
141
+ end
142
+ end