parlour 0.1.0 → 0.1.1

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: 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