parlour 1.0.0 → 4.0.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  3. data/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  4. data/.gitignore +1 -0
  5. data/.parlour +5 -0
  6. data/.rspec +0 -0
  7. data/.travis.yml +4 -3
  8. data/CHANGELOG.md +65 -0
  9. data/CODE_OF_CONDUCT.md +0 -0
  10. data/Gemfile +0 -0
  11. data/LICENSE.txt +0 -0
  12. data/README.md +54 -1
  13. data/Rakefile +0 -0
  14. data/exe/parlour +68 -2
  15. data/lib/parlour.rb +7 -0
  16. data/lib/parlour/conflict_resolver.rb +129 -19
  17. data/lib/parlour/debugging.rb +0 -0
  18. data/lib/parlour/detached_rbi_generator.rb +25 -0
  19. data/lib/parlour/kernel_hack.rb +2 -0
  20. data/lib/parlour/parse_error.rb +19 -0
  21. data/lib/parlour/plugin.rb +1 -1
  22. data/lib/parlour/rbi_generator.rb +13 -7
  23. data/lib/parlour/rbi_generator/arbitrary.rb +0 -0
  24. data/lib/parlour/rbi_generator/attribute.rb +0 -0
  25. data/lib/parlour/rbi_generator/class_namespace.rb +8 -5
  26. data/lib/parlour/rbi_generator/constant.rb +11 -2
  27. data/lib/parlour/rbi_generator/enum_class_namespace.rb +24 -2
  28. data/lib/parlour/rbi_generator/extend.rb +0 -0
  29. data/lib/parlour/rbi_generator/include.rb +0 -0
  30. data/lib/parlour/rbi_generator/method.rb +0 -0
  31. data/lib/parlour/rbi_generator/module_namespace.rb +6 -4
  32. data/lib/parlour/rbi_generator/namespace.rb +81 -15
  33. data/lib/parlour/rbi_generator/options.rb +15 -2
  34. data/lib/parlour/rbi_generator/parameter.rb +5 -5
  35. data/lib/parlour/rbi_generator/rbi_object.rb +0 -0
  36. data/lib/parlour/rbi_generator/struct_class_namespace.rb +103 -0
  37. data/lib/parlour/rbi_generator/struct_prop.rb +136 -0
  38. data/lib/parlour/type_loader.rb +104 -0
  39. data/lib/parlour/type_parser.rb +854 -0
  40. data/lib/parlour/version.rb +1 -1
  41. data/parlour.gemspec +6 -5
  42. data/plugin_examples/foobar_plugin.rb +0 -0
  43. data/rbi/parlour.rbi +893 -0
  44. metadata +40 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f603ddca16331f43a36b4120b7d12aa5a4b7c83f753e8527b91bc1391109e9fe
4
- data.tar.gz: f550cdf4cc358dd6ac0c21853ad26756d4729c7f28f3695af53d25775b3f45f0
3
+ metadata.gz: 707934b89fd83fbb876582bd7e66679bc2f8cf2179961eed62358ba6266bf524
4
+ data.tar.gz: 3d0a0c8be95e09275a9b02232ce9b9b062a9f00eb3216a0e74ce63a036025d76
5
5
  SHA512:
6
- metadata.gz: a962191c42becbe6dbe213eb63be142df100643786006093e5663af51f80049890c26ff8974b718cfd89198387cf0ec801b8d3e10cbf032aaf3aa86f746fda97
7
- data.tar.gz: 1bfafd7afbcf81105379d7b747e0c204e47fdd593b0b1a67e659275b0ebd8d236753e48b5bc05896e1e1759ac6495ad3e8132f85a497f28b35c436ddb3cb1585
6
+ metadata.gz: ae723cbe6bebbba12ee3d802b223fa91fcda085f6b1fce4ec107efa3f9e3cc5df53ee0269a6e9d1391ff32c903d15b72153c67fae61392b7e5495688b69b7b0a
7
+ data.tar.gz: 98626719eb9d2c08f7e87c1ae48933d8b1afb9badab000bb408a8df26c57716846b71278654193c821ce59a552c83456d1f61523ce5bd9ff0e52d296ab34e274
File without changes
File without changes
data/.gitignore CHANGED
@@ -8,6 +8,7 @@
8
8
  /tmp/
9
9
  Gemfile.lock
10
10
  /.vscode
11
+ /sorbet/rbi/hidden-definitions/errors.txt
11
12
 
12
13
  # rspec failure tracking
13
14
  .rspec_status
@@ -0,0 +1,5 @@
1
+ excluded_paths:
2
+ - lib/parlour/kernel_hack.rb
3
+
4
+ excluded_modules:
5
+ - Parlour::DetachedRbiGenerator
data/.rspec CHANGED
File without changes
@@ -8,10 +8,8 @@ rvm:
8
8
  - 2.4
9
9
  - 2.5
10
10
  - 2.6
11
+ - 2.7
11
12
  - ruby-head
12
- matrix:
13
- allow_failures:
14
- - rvm: ruby-head
15
13
 
16
14
  jobs:
17
15
  include:
@@ -25,3 +23,6 @@ jobs:
25
23
  keep_history: true
26
24
  on:
27
25
  branch: master
26
+ allow_failures:
27
+ - rvm: 2.3
28
+ - rvm: ruby-head
@@ -3,6 +3,71 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [4.0.1] - 2020-08-05
7
+ ### Fixed
8
+ - Fixed duplicate includes and extends.
9
+ - Fixed the block return type for `#resolve_conflicts` not being nilable.
10
+
11
+ ## [4.0.0] - 2020-05-23
12
+ ### Added
13
+ - Parlour now defaults to loading the current project when running its command
14
+ line tool, allowing it to be used as a "`sig` extractor" when run without
15
+ plugins! **Breaking if you invoke Parlour from its command line tool** - to
16
+ revert to the old behaviour of having nothing loaded into the root namespace
17
+ initially, add `parser: false` to your `.parlour` file.
18
+ - Generating constants in an eigenclass context (`class << self`) is now
19
+ supported.
20
+
21
+ ## [3.0.0] - 2020-05-15
22
+ ### Added
23
+ - `T::Struct` classes can now be generated and parsed.
24
+ - `T::Enum` classes can now be parsed.
25
+ - Constants are now parsed.
26
+ - `TypeParser` now detects and parses methods which do not have a `sig`.
27
+ **Potentially breaking if there is a strict set of methods you are expecting Parlour to detect.**
28
+
29
+ ### Fixed
30
+ - "Specialized" classes, such as enums and now structs, have had many erroneous
31
+ conflicts with standard classes or namespaces fixed.
32
+ - Attributes writers and methods with the same name no longer conflict incorrectly.
33
+
34
+ ## [2.1.0] - 2020-03-22
35
+ ### Added
36
+ - Files can now be excluded from the `TypeLoader`.
37
+
38
+ ### Changed
39
+ - A block argument in the definition but not in the signature no longer causes
40
+ an error in the `TypeParser`.
41
+ - Sorting of namespace children is now a stable sort.
42
+
43
+ ### Fixed
44
+ - Type parameters are now parsed by the `TypeParser`.
45
+
46
+ ## [2.0.0] - 2020-02-10
47
+ ### Added
48
+ - Parlour can now load types back out of RBI files or Ruby source files by
49
+ parsing them, using the `TypeLoader` module.
50
+ - The `sort_namespaces` option has been added to `RbiGenerator` to
51
+ alphabetically sort all namespace children.
52
+ - Added `DetachedRbiGenerator`, which can be used to create instances of
53
+ `RbiObject` which are not bound to a particular set of options. This is
54
+ used internally for `TypeLoader`.
55
+ - Parlour will now create a polyfill for `then` on `Kernel`.
56
+ - Added `NodePath#sibling`.
57
+
58
+ ### Changed
59
+ - Version restrictions on _rainbow_ and _commander_ have been slightly relaxed.
60
+ - The version of _sorbet-runtime_ is now restricted to `>= 0.5` after previously
61
+ being unrestricted.
62
+ - Instances of `Namespace` can now be merged with instances of `ClassNamespace`
63
+ or `MethodNamespace`.
64
+ - A method and a namespace can now have the same name without causing a merge
65
+ conflict.
66
+
67
+ ### Fixed
68
+ - Parameter names are no longer nilable.
69
+ **Potentially breaking if you were doing something cursed with Parameter names.**
70
+
6
71
  ## [1.0.0] - 2019-11-22
7
72
  ### Added
8
73
  - `T::Enum` classes have been implemented, and can be generated using
File without changes
data/Gemfile CHANGED
File without changes
File without changes
data/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  [![Build Status](https://travis-ci.org/AaronC81/parlour.svg?branch=master)](https://travis-ci.org/AaronC81/parlour)
4
4
  ![Gem](https://img.shields.io/gem/v/parlour.svg)
5
5
 
6
- Parlour is an RBI generator and merger for Sorbet. It consists of two key parts:
6
+ Parlour is an RBI generator, merger and parser for Sorbet. It consists of three
7
+ key parts:
7
8
 
8
9
  - The generator, which outputs beautifully formatted RBI files, created using
9
10
  an intuitive DSL.
@@ -12,6 +13,9 @@ Parlour is an RBI generator and merger for Sorbet. It consists of two key parts:
12
13
  RBIs for the same codebase. These are combined automatically as much as
13
14
  possible, but any other conflicts can be resolved manually through prompts.
14
15
 
16
+ - The parser, which can read an RBI and convert it back into a tree of
17
+ generator objects.
18
+
15
19
  ## Why should I use this?
16
20
 
17
21
  - Parlour enables **much easier creation of RBI generators**, as formatting
@@ -21,6 +25,11 @@ Parlour is an RBI generator and merger for Sorbet. It consists of two key parts:
21
25
  single command and consolidating all of their definitions into a single
22
26
  RBI output file.
23
27
 
28
+ - You can **effortlessly build tools which need to access types within an RBI**;
29
+ no need to write your own parser!
30
+
31
+ - You can **generate RBI to ship with your gem** for consuming projects to use
32
+ ([see "RBIs within gems" in Sorbet's docs](https://sorbet.org/docs/rbi#rbis-within-gems)).
24
33
 
25
34
  Please [**read the wiki**](https://github.com/AaronC81/parlour/wiki) to get
26
35
  started!
@@ -147,6 +156,50 @@ plugins:
147
156
  Gem3::Plugin: {}
148
157
  ```
149
158
 
159
+ ## Parsing RBIs
160
+
161
+ You can either parse individual RBI files, or point Parlour to the root of a
162
+ project and it will locate, parse and merge all RBI files.
163
+
164
+ Note that Parlour isn't limited to just RBIs; it can parse inline `sigs` out
165
+ of your Ruby source too!
166
+
167
+ ```ruby
168
+ require 'parlour'
169
+
170
+ # Return the object tree of a particular file
171
+ Parlour::TypeLoader.load_file('path/to/your/file.rbis')
172
+
173
+ # Return the object tree for an entire Sorbet project - slow but thorough!
174
+ Parlour::TypeLoader.load_project('root/of/the/project')
175
+ ```
176
+
177
+ The structure of the returned object trees is identical to those you would
178
+ create when generating an RBI, built of instances of `RbiObject` subclasses.
179
+
180
+ ## Generating RBI for a Gem
181
+
182
+ Include `parlour` as a development_dependency in your `.gemspec`:
183
+
184
+ ```ruby
185
+ spec.add_development_dependency 'parlour'
186
+ ```
187
+
188
+ Run Parlour from the command line:
189
+
190
+ ```ruby
191
+ bundle exec parlour
192
+ ```
193
+
194
+ Parlour is configured to use sane defaults assuming a standard gem structure
195
+ to generate an RBI that Sorbet will automatically find when your gem is included
196
+ as a dependency. If you require more advanced configuration you can add a
197
+ `.parlour` YAML file in the root of your project (see this project's `.parlour`
198
+ file as an example).
199
+
200
+ To disable the parsing step entire and just run plugins you can set `parser: false`
201
+ in your `.parlour` file.
202
+
150
203
  ## Parlour Plugins
151
204
 
152
205
  _Have you written an awesome Parlour plugin? Please submit a PR to add it to this list!_
data/Rakefile CHANGED
File without changes
@@ -16,15 +16,42 @@ command :run do |c|
16
16
  c.description = 'Generates an RBI file from your .parlour file'
17
17
 
18
18
  c.action do |args, options|
19
- configuration = keys_to_symbols(YAML.load_file(File.join(Dir.pwd, '.parlour')))
19
+ working_dir = Dir.pwd
20
+ config_filename = File.join(working_dir, '.parlour')
20
21
 
21
- raise 'you must specify output_file in your .parlour file' unless configuration[:output_file]
22
+ if File.exists?(config_filename)
23
+ configuration = keys_to_symbols(YAML.load_file(config_filename))
24
+ else
25
+ configuration = {}
26
+ end
27
+
28
+ # Output default
29
+ configuration[:output_file] ||= "rbi/#{File.basename(working_dir)}.rbi"
22
30
 
23
31
  # Style defaults
24
32
  configuration[:style] ||= {}
25
33
  configuration[:style][:tab_size] ||= 2
26
34
  configuration[:style][:break_params] ||= 4
27
35
 
36
+ # Parser defaults, set explicitly to false to not run parser
37
+ if configuration[:parser] != false
38
+ configuration[:parser] ||= {}
39
+
40
+ # Input/Output defaults
41
+ configuration[:parser][:root] ||= '.'
42
+
43
+ # Included/Excluded path defaults
44
+ configuration[:parser][:included_paths] ||= ['lib']
45
+ configuration[:parser][:excluded_paths] ||= ['sorbet', 'spec']
46
+
47
+ # Defaults can be overridden but we always want to exclude the output file
48
+ configuration[:parser][:excluded_paths] << configuration[:output_file]
49
+ end
50
+
51
+ # Included/Excluded module defaults
52
+ configuration[:included_modules] ||= []
53
+ configuration[:excluded_modules] ||= []
54
+
28
55
  # Require defaults
29
56
  configuration[:requires] ||= []
30
57
  configuration[:relative_requires] ||= []
@@ -53,6 +80,15 @@ command :run do |c|
53
80
  break_params: configuration[:style][:break_params],
54
81
  tab_size: configuration[:style][:tab_size]
55
82
  )
83
+
84
+ if configuration[:parser]
85
+ Parlour::TypeLoader.load_project(
86
+ configuration[:parser][:root],
87
+ inclusions: configuration[:parser][:included_paths],
88
+ exclusions: configuration[:parser][:excluded_paths],
89
+ generator: gen,
90
+ )
91
+ end
56
92
  Parlour::Plugin.run_plugins(plugin_instances, gen)
57
93
 
58
94
  # Run a pass of the conflict resolver
@@ -74,6 +110,14 @@ command :run do |c|
74
110
  choice == 0 ? nil : candidates[choice - 1]
75
111
  end
76
112
 
113
+ if !configuration[:included_modules].empty? || !configuration[:excluded_modules].empty?
114
+ remove_unwanted_modules(
115
+ gen.root,
116
+ included_modules: configuration[:included_modules],
117
+ excluded_modules: configuration[:excluded_modules],
118
+ )
119
+ end
120
+
77
121
  # Figure out strictness levels
78
122
  requested_strictness_levels = plugin_instances.map do |plugin|
79
123
  s = plugin.strictness&.to_s
@@ -98,6 +142,7 @@ command :run do |c|
98
142
  end
99
143
 
100
144
  # Write the final RBI
145
+ FileUtils.mkdir_p(File.dirname(configuration[:output_file]))
101
146
  File.write(configuration[:output_file], gen.rbi(strictness))
102
147
  end
103
148
  end
@@ -122,3 +167,24 @@ def keys_to_symbols(hash)
122
167
  ]
123
168
  end.to_h
124
169
  end
170
+
171
+ def remove_unwanted_modules(root, included_modules:, excluded_modules:, prefix: nil)
172
+ root.children.select! do |child|
173
+ module_name = "#{prefix}#{child.name}"
174
+
175
+ if child.respond_to?(:children)
176
+ remove_unwanted_modules(
177
+ child,
178
+ included_modules: included_modules,
179
+ excluded_modules: excluded_modules,
180
+ prefix: "#{module_name}::",
181
+ )
182
+ has_included_children = !child.children.empty?
183
+ end
184
+
185
+ included = included_modules.empty? ? true : included_modules.any? { |m| module_name.start_with?(m) }
186
+ excluded = excluded_modules.empty? ? false : excluded_modules.any? { |m| module_name.start_with?(m) }
187
+
188
+ (included || has_included_children) && !excluded
189
+ end
190
+ end
@@ -22,6 +22,13 @@ require 'parlour/rbi_generator/namespace'
22
22
  require 'parlour/rbi_generator/module_namespace'
23
23
  require 'parlour/rbi_generator/class_namespace'
24
24
  require 'parlour/rbi_generator/enum_class_namespace'
25
+ require 'parlour/rbi_generator/struct_prop'
26
+ require 'parlour/rbi_generator/struct_class_namespace'
25
27
  require 'parlour/rbi_generator'
28
+ require 'parlour/detached_rbi_generator'
26
29
 
27
30
  require 'parlour/conflict_resolver'
31
+
32
+ require 'parlour/parse_error'
33
+ require 'parlour/type_parser'
34
+ require 'parlour/type_loader'
@@ -1,4 +1,6 @@
1
1
  # typed: true
2
+ require 'set'
3
+
2
4
  module Parlour
3
5
  # Responsible for resolving conflicts (that is, multiple definitions with the
4
6
  # same name) between objects defined in the same namespace.
@@ -11,7 +13,7 @@ module Parlour
11
13
  resolver: T.proc.params(
12
14
  desc: String,
13
15
  choices: T::Array[RbiGenerator::RbiObject]
14
- ).returns(RbiGenerator::RbiObject)
16
+ ).returns(T.nilable(RbiGenerator::RbiObject))
15
17
  ).void
16
18
  end
17
19
  # Given a namespace, attempts to automatically resolve conflicts in the
@@ -21,13 +23,13 @@ module Parlour
21
23
  # All children of the given namespace which are also namespaces are
22
24
  # processed recursively, so passing {RbiGenerator#root} will eliminate all
23
25
  # conflicts in the entire object tree.
24
- #
26
+ #
25
27
  # If automatic resolution is not possible, the block passed to this method
26
28
  # is invoked and passed two arguments: a message on what the conflict is,
27
29
  # and an array of candidate objects. The block should return one of these
28
30
  # candidate objects, which will be kept, and all other definitions are
29
31
  # deleted. Alternatively, the block may return nil, which will delete all
30
- # definitions. The block may be invoked many times from one call to
32
+ # definitions. The block may be invoked many times from one call to
31
33
  # {resolve_conflicts}, one for each unresolvable conflict.
32
34
  #
33
35
  # @param namespace [RbiGenerator::Namespace] The starting namespace to
@@ -42,7 +44,14 @@ module Parlour
42
44
  Debugging.debug_puts(self, Debugging::Tree.begin("Resolving conflicts for #{namespace.name}..."))
43
45
 
44
46
  # Check for multiple definitions with the same name
45
- grouped_by_name_children = namespace.children.group_by(&:name)
47
+ # (Special case here: writer attributes get an "=" appended to their name)
48
+ grouped_by_name_children = namespace.children.group_by do |child|
49
+ if RbiGenerator::Attribute === child && child.kind == :writer
50
+ "#{child.name}=" unless child.name.end_with?('=')
51
+ else
52
+ child.name
53
+ end
54
+ end
46
55
 
47
56
  grouped_by_name_children.each do |name, children|
48
57
  Debugging.debug_puts(self, Debugging::Tree.begin("Checking children named #{name}..."))
@@ -50,7 +59,7 @@ module Parlour
50
59
  if children.length > 1
51
60
  Debugging.debug_puts(self, Debugging::Tree.here("Possible conflict between #{children.length} objects"))
52
61
 
53
- # Special case: do we have two methods, one of which is a class method
62
+ # Special case: do we have two methods, one of which is a class method
54
63
  # and the other isn't? If so, do nothing - this is fine
55
64
  if children.length == 2 &&
56
65
  children.all? { |c| c.is_a?(RbiGenerator::Method) } &&
@@ -60,7 +69,22 @@ module Parlour
60
69
  next
61
70
  end
62
71
 
63
- # Special case: do we have two attributes, one of which is a class
72
+ # Special case: if we remove the namespaces, is everything either an
73
+ # include or an extend? If so, do nothing - this is fine
74
+ if children \
75
+ .reject { |c| c.is_a?(RbiGenerator::Namespace) }
76
+ .then do |x|
77
+ !x.empty? && x.all? do |c|
78
+ c.is_a?(RbiGenerator::Include) || c.is_a?(RbiGenerator::Extend)
79
+ end
80
+ end
81
+ deduplicate_mixins_of_name(namespace, name)
82
+
83
+ Debugging.debug_puts(self, Debugging::Tree.end("Includes/extends do not conflict with namespaces; no resolution required"))
84
+ next
85
+ end
86
+
87
+ # Special case: do we have two attributes, one of which is a class
64
88
  # attribute and the other isn't? If so, do nothing - this is fine
65
89
  if children.length == 2 &&
66
90
  children.all? { |c| c.is_a?(RbiGenerator::Attribute) } &&
@@ -70,13 +94,13 @@ module Parlour
70
94
  next
71
95
  end
72
96
 
73
- # Special case: are they all clearly equal? If so, remove all but one
97
+ # Optimization for Special case: are they all clearly equal? If so, remove all but one
74
98
  if all_eql?(children)
75
99
  Debugging.debug_puts(self, Debugging::Tree.end("All children are identical"))
76
100
 
77
101
  # All of the children are the same, so this deletes all of them
78
102
  namespace.children.delete(T.must(children.first))
79
-
103
+
80
104
  # Re-add one child
81
105
  namespace.children << T.must(children.first)
82
106
  next
@@ -88,11 +112,11 @@ module Parlour
88
112
  namespace.children.delete(c)
89
113
  end
90
114
 
91
- # We can only try to resolve automatically if they're all the same
92
- # type of object, so check that first
93
- children_type = single_type_of_array(children)
94
- unless children_type
95
- Debugging.debug_puts(self, Debugging::Tree.end("Children are different types; requesting manual resolution"))
115
+ # Check that the types of the given objects allow them to be merged,
116
+ # and get the strategy to use
117
+ strategy = merge_strategy(children)
118
+ unless strategy
119
+ Debugging.debug_puts(self, Debugging::Tree.end("Children are unmergeable types; requesting manual resolution"))
96
120
  # The types aren't the same, so ask the resolver what to do, and
97
121
  # insert that (if not nil)
98
122
  choice = resolver.call("Different kinds of definition for the same name", children)
@@ -100,8 +124,47 @@ module Parlour
100
124
  next
101
125
  end
102
126
 
127
+ case strategy
128
+ when :normal
129
+ first, *rest = children
130
+ when :differing_namespaces
131
+ # Let the namespaces be merged normally, but handle the method here
132
+ namespaces, non_namespaces = children.partition { |x| RbiGenerator::Namespace === x }
133
+
134
+ # If there is any non-namespace item in this conflict, it should be
135
+ # a single method
136
+ if non_namespaces.length != 0
137
+ unless non_namespaces.length == 1 && RbiGenerator::Method === non_namespaces.first
138
+ Debugging.debug_puts(self, Debugging::Tree.end("Non-namespace item in a differing namespace conflict is not a single method; requesting manual resolution"))
139
+ # The types aren't the same, so ask the resolver what to do, and
140
+ # insert that (if not nil)
141
+ choice = resolver.call("Non-namespace item in a differing namespace conflict is not a single method", non_namespaces)
142
+ non_namespaces = []
143
+ non_namespaces << choice if choice
144
+ end
145
+ end
146
+
147
+ non_namespaces.each do |x|
148
+ namespace.children << x
149
+ end
150
+
151
+ # For certain namespace types the order matters. For example, if there's
152
+ # both a `Namespace` and `ModuleNamespace` then merging the two would
153
+ # produce different results depending on which is first.
154
+ first_index = (
155
+ namespaces.find_index { |x| RbiGenerator::EnumClassNamespace === x || RbiGenerator::StructClassNamespace === x } ||
156
+ namespaces.find_index { |x| RbiGenerator::ClassNamespace === x } ||
157
+ namespaces.find_index { |x| RbiGenerator::ModuleNamespace === x } ||
158
+ 0
159
+ )
160
+
161
+ first = namespaces.delete_at(first_index)
162
+ rest = namespaces
163
+ else
164
+ raise 'unknown merge strategy; this is a Parlour bug'
165
+ end
166
+
103
167
  # Can the children merge themselves automatically? If so, let them
104
- first, *rest = children
105
168
  first, rest = T.must(first), T.must(rest)
106
169
  if T.must(first).mergeable?(T.must(rest))
107
170
  Debugging.debug_puts(self, Debugging::Tree.end("Children are all mergeable; resolving automatically"))
@@ -131,15 +194,41 @@ module Parlour
131
194
 
132
195
  private
133
196
 
134
- sig { params(arr: T::Array[T.untyped]).returns(T.nilable(Class)) }
197
+ sig { params(arr: T::Array[T.untyped]).returns(T.nilable(Symbol)) }
135
198
  # Given an array, if all elements in the array are instances of the exact
136
- # same class, returns that class. If they are not, returns nil.
199
+ # same class or are otherwise mergeable (for example Namespace and
200
+ # ClassNamespace), returns the kind of merge which needs to be made. A
201
+ # return value of nil indicates that the values cannot be merged.
202
+ #
203
+ # The following kinds are available:
204
+ # - They are all the same. (:normal)
205
+ # - There are exactly two types, one of which is Namespace and other is a
206
+ # subclass of it. (:differing_namespaces)
207
+ # - One of them is Namespace or a subclass (or both, as described above),
208
+ # and the only other is Method. (also :differing_namespaces)
137
209
  #
138
210
  # @param arr [Array] The array.
139
- # @return [Class, nil] Either a class, or nil.
140
- def single_type_of_array(arr)
211
+ # @return [Symbol] The merge strategy to use, or nil if they can't be
212
+ # merged.
213
+ def merge_strategy(arr)
214
+ # If they're all the same type, they can be merged easily
141
215
  array_types = arr.map { |c| c.class }.uniq
142
- array_types.length == 1 ? array_types.first : nil
216
+ return :normal if array_types.length == 1
217
+
218
+ # Find all the namespaces and non-namespaces
219
+ namespace_types, non_namespace_types = array_types.partition { |x| x <= RbiGenerator::Namespace }
220
+ exactly_namespace, namespace_subclasses = namespace_types.partition { |x| x == RbiGenerator::Namespace }
221
+
222
+ return nil unless namespace_subclasses.empty? \
223
+ || (namespace_subclasses.length == 1 && namespace_subclasses.first < RbiGenerator::Namespace) \
224
+ || namespace_subclasses.to_set == Set[RbiGenerator::ClassNamespace, RbiGenerator::StructClassNamespace] \
225
+ || namespace_subclasses.to_set == Set[RbiGenerator::ClassNamespace, RbiGenerator::EnumClassNamespace]
226
+
227
+ # It's OK, albeit cursed, for there to be a method with the same name as
228
+ # a namespace (Rainbow does this)
229
+ return nil if non_namespace_types.length != 0 && non_namespace_types != [RbiGenerator::Method]
230
+
231
+ :differing_namespaces
143
232
  end
144
233
 
145
234
  sig { params(arr: T::Array[T.untyped]).returns(T::Boolean) }
@@ -151,5 +240,26 @@ module Parlour
151
240
  def all_eql?(arr)
152
241
  arr.each_cons(2).all? { |x, y| x == y }
153
242
  end
243
+
244
+ sig { params(namespace: RbiGenerator::Namespace, name: T.nilable(String)).void }
245
+ # Given a namespace and a child name, removes all duplicate children that are mixins
246
+ # and that have the given name, except the first found instance.
247
+ #
248
+ # @param namespace [RbiGenerator::Namespace] The namespace to deduplicate mixins in.
249
+ # @param name [String] The name of the mixin modules to deduplicate.
250
+ # @return [void]
251
+ def deduplicate_mixins_of_name(namespace, name)
252
+ found_map = {}
253
+ namespace.children.delete_if do |x|
254
+ # ignore children whose names don't match
255
+ next unless x.name == name
256
+ # ignore children that are not mixins
257
+ next unless x.is_a?(RbiGenerator::Include) || x.is_a?(RbiGenerator::Extend)
258
+
259
+ delete = found_map.key?(x.class)
260
+ found_map[x.class] = true
261
+ delete
262
+ end
263
+ end
154
264
  end
155
265
  end