parlour 2.0.0 → 5.0.0.beta.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 (68) 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 -1
  5. data/.parlour +5 -0
  6. data/.rspec +0 -0
  7. data/.travis.yml +3 -3
  8. data/CHANGELOG.md +64 -0
  9. data/CODE_OF_CONDUCT.md +0 -0
  10. data/Gemfile +0 -0
  11. data/LICENSE.txt +0 -0
  12. data/README.md +233 -19
  13. data/Rakefile +0 -0
  14. data/exe/parlour +109 -4
  15. data/lib/parlour.rb +29 -1
  16. data/lib/parlour/conflict_resolver.rb +75 -27
  17. data/lib/parlour/conversion/converter.rb +34 -0
  18. data/lib/parlour/conversion/rbi_to_rbs.rb +223 -0
  19. data/lib/parlour/debugging.rb +0 -0
  20. data/lib/parlour/detached_rbi_generator.rb +1 -6
  21. data/lib/parlour/detached_rbs_generator.rb +25 -0
  22. data/lib/parlour/generator.rb +34 -0
  23. data/lib/parlour/kernel_hack.rb +0 -0
  24. data/lib/parlour/options.rb +71 -0
  25. data/lib/parlour/parse_error.rb +0 -0
  26. data/lib/parlour/plugin.rb +1 -1
  27. data/lib/parlour/rbi_generator.rb +24 -37
  28. data/lib/parlour/rbi_generator/arbitrary.rb +5 -2
  29. data/lib/parlour/rbi_generator/attribute.rb +14 -5
  30. data/lib/parlour/rbi_generator/class_namespace.rb +8 -3
  31. data/lib/parlour/rbi_generator/constant.rb +28 -8
  32. data/lib/parlour/rbi_generator/enum_class_namespace.rb +32 -5
  33. data/lib/parlour/rbi_generator/extend.rb +5 -2
  34. data/lib/parlour/rbi_generator/include.rb +5 -2
  35. data/lib/parlour/rbi_generator/method.rb +15 -10
  36. data/lib/parlour/rbi_generator/module_namespace.rb +7 -2
  37. data/lib/parlour/rbi_generator/namespace.rb +115 -27
  38. data/lib/parlour/rbi_generator/parameter.rb +13 -7
  39. data/lib/parlour/rbi_generator/rbi_object.rb +19 -78
  40. data/lib/parlour/rbi_generator/struct_class_namespace.rb +110 -0
  41. data/lib/parlour/rbi_generator/struct_prop.rb +139 -0
  42. data/lib/parlour/rbi_generator/type_alias.rb +101 -0
  43. data/lib/parlour/rbs_generator.rb +24 -0
  44. data/lib/parlour/rbs_generator/arbitrary.rb +92 -0
  45. data/lib/parlour/rbs_generator/attribute.rb +82 -0
  46. data/lib/parlour/rbs_generator/block.rb +49 -0
  47. data/lib/parlour/rbs_generator/class_namespace.rb +106 -0
  48. data/lib/parlour/rbs_generator/constant.rb +95 -0
  49. data/lib/parlour/rbs_generator/extend.rb +92 -0
  50. data/lib/parlour/rbs_generator/include.rb +92 -0
  51. data/lib/parlour/rbs_generator/interface_namespace.rb +34 -0
  52. data/lib/parlour/rbs_generator/method.rb +146 -0
  53. data/lib/parlour/rbs_generator/method_signature.rb +104 -0
  54. data/lib/parlour/rbs_generator/module_namespace.rb +35 -0
  55. data/lib/parlour/rbs_generator/namespace.rb +627 -0
  56. data/lib/parlour/rbs_generator/parameter.rb +145 -0
  57. data/lib/parlour/rbs_generator/rbs_object.rb +78 -0
  58. data/lib/parlour/rbs_generator/type_alias.rb +96 -0
  59. data/lib/parlour/type_loader.rb +30 -10
  60. data/lib/parlour/type_parser.rb +440 -43
  61. data/lib/parlour/typed_object.rb +87 -0
  62. data/lib/parlour/types.rb +445 -0
  63. data/lib/parlour/version.rb +1 -1
  64. data/parlour.gemspec +2 -2
  65. data/plugin_examples/foobar_plugin.rb +0 -0
  66. data/rbi/parlour.rbi +1799 -0
  67. metadata +42 -15
  68. data/lib/parlour/rbi_generator/options.rb +0 -74
@@ -0,0 +1,145 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbsGenerator < Generator
4
+ # Represents a method parameter with a Sorbet type signature.
5
+ class Parameter
6
+ extend T::Sig
7
+
8
+ sig do
9
+ params(
10
+ name: String,
11
+ type: T.nilable(Types::TypeLike),
12
+ required: T::Boolean,
13
+ ).void
14
+ end
15
+ # Create a new method parameter.
16
+ # Note that, in RBS, blocks are not parameters. Use a {Block} instead.
17
+ #
18
+ # @example Create a simple Integer parameter named +num+.
19
+ # Parlour::RbsGenerator::Parameter.new('num', type: 'Integer')
20
+ # @example Create a nilable array parameter.
21
+ # Parlour::RbsGenerator::Parameter.new('array_of_strings_or_symbols', type:
22
+ # Parlour::Types::Nilable.new(
23
+ # Parlour::Types::Array.new(
24
+ # Parlour::Types::Union.new('String', 'Symbol')
25
+ # )
26
+ # )
27
+ # )
28
+ # @example Create an optional parameter.
29
+ # Parlour::RbsGenerator::Parameter.new('name', type: 'String', default: 'Parlour')
30
+ #
31
+ # @param name [String] The name of this parameter. This may start with +*+ or +**+,
32
+ # ,or end with +:+, which will infer the {kind} of this
33
+ # parameter. (If it contains none of those, {kind} will be +:normal+.)
34
+ # @param type [Types::TypeLike, nil] This type of this parameter.
35
+ # @param required [Boolean] Whether this parameter is required.
36
+ # @return [void]
37
+ def initialize(name, type: nil, required: true)
38
+ name = T.must(name)
39
+ @name = name
40
+
41
+ prefix = /^(\*\*|\*|\&)?/.match(name)&.captures&.first || ''
42
+ @kind = PREFIXES.rassoc(prefix).first
43
+
44
+ @kind = :keyword if kind == :normal && name.end_with?(':')
45
+
46
+ @type = type || Parlour::Types::Untyped.new
47
+ @required = required
48
+ end
49
+
50
+ sig { params(other: Object).returns(T::Boolean) }
51
+ # Returns true if this instance is equal to another method.
52
+ #
53
+ # @param other [Object] The other instance. If this is not a {Parameter} (or a
54
+ # subclass of it), this will always return false.
55
+ # @return [Boolean]
56
+ def ==(other)
57
+ Parameter === other &&
58
+ name == other.name &&
59
+ kind == other.kind &&
60
+ type == other.type &&
61
+ required == other.required
62
+ end
63
+
64
+ sig { returns(String) }
65
+ # The name of this parameter, including any prefixes or suffixes such as
66
+ # +*+.
67
+ # @return [String]
68
+ attr_reader :name
69
+
70
+ sig { returns(String) }
71
+ # The name of this parameter, stripped of any prefixes or suffixes. For
72
+ # example, +*rest+ would become +rest+, or +foo:+ would become +foo+.
73
+ #
74
+ # @return [String]
75
+ def name_without_kind
76
+ return T.must(name[0..-2]) if kind == :keyword
77
+
78
+ prefix_match = /^(\*\*|\*|\&)?[a-zA-Z_]/.match(name)
79
+ raise 'unknown prefix' unless prefix_match
80
+ prefix = prefix_match.captures.first || ''
81
+ T.must(name[prefix.length..-1])
82
+ end
83
+
84
+ sig { returns(Types::TypeLike) }
85
+ # This parameter's type.
86
+ # @return [String]
87
+ attr_reader :type
88
+
89
+ sig { returns(T::Boolean) }
90
+ # Whether this parameter is required.
91
+ # @return [Boolean]
92
+ attr_reader :required
93
+
94
+ sig { returns(Symbol) }
95
+ # The kind of parameter that this is. This will be one of +:normal+,
96
+ # +:splat+, +:double_splat+, or +:keyword+.
97
+ # @return [Symbol]
98
+ attr_reader :kind
99
+
100
+ # An array of reserved keywords in RBS which may be used as parameter
101
+ # names in standard Ruby.
102
+ # TODO: probably incomplete
103
+ RBS_KEYWORDS = [
104
+ 'type', 'interface', 'out', 'in', 'instance'
105
+ ]
106
+
107
+ # A mapping of {kind} values to the characteristic prefixes each kind has.
108
+ PREFIXES = {
109
+ normal: '',
110
+ splat: '*',
111
+ double_splat: '**',
112
+ }.freeze
113
+
114
+ sig { returns(String) }
115
+ # A string of how this parameter should be defined in an RBS signature.
116
+ #
117
+ # @return [String]
118
+ def to_rbs_param
119
+ raise 'blocks are not parameters in RBS' if kind == :block
120
+
121
+ t = String === @type ? @type : @type.generate_rbs
122
+ t = "^#{t}" if Types::Proc === @type
123
+
124
+ if RBS_KEYWORDS.include? name_without_kind
125
+ unless $VERBOSE.nil?
126
+ print Rainbow("Parlour warning: ").yellow.dark.bold
127
+ print Rainbow("Type generalization: ").magenta.bright.bold
128
+ puts "'#{name_without_kind}' is a keyword in RBS, renaming method parameter to '_#{name_without_kind}'"
129
+ end
130
+
131
+ n = "_#{name_without_kind}"
132
+ else
133
+ n = name_without_kind
134
+ end
135
+
136
+ # Extra check because "?*something" is invalid
137
+ ((required || (kind != :normal && kind != :keyword)) ? '' : '?') + if kind == :keyword
138
+ "#{n}: #{t}"
139
+ else
140
+ "#{PREFIXES[kind]}#{t} #{n}"
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,78 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbsGenerator < Generator
4
+ # An abstract class which is subclassed by any classes which can generate
5
+ # entire lines of an RBS, such as {Namespace} and {Method}. (As an example,
6
+ # {Parameter} is _not_ a subclass because it does not generate lines, only
7
+ # segments of definition lines.)
8
+ # @abstract
9
+ class RbsObject < TypedObject
10
+ abstract!
11
+
12
+ sig { params(generator: Generator, name: String).void }
13
+ # Creates a new RBS object.
14
+ # @note Don't call this directly.
15
+ #
16
+ # @param generator [RbsGenerator] The current RbsGenerator.
17
+ # @param name [String] The name of this module.
18
+ # @return [void]
19
+ def initialize(generator, name)
20
+ super(name)
21
+ @generator = generator
22
+ @generated_by = RbsGenerator === generator ? generator.current_plugin : nil
23
+ end
24
+
25
+ sig { returns(Generator) }
26
+ # The generator which this object belongs to.
27
+ # @return [Generator]
28
+ attr_reader :generator
29
+
30
+ sig do
31
+ abstract.params(
32
+ indent_level: Integer,
33
+ options: Options
34
+ ).returns(T::Array[String])
35
+ end
36
+ # Generates the RBS lines for this object.
37
+ #
38
+ # @abstract
39
+ # @param indent_level [Integer] The indentation level to generate the lines at.
40
+ # @param options [Options] The formatting options to use.
41
+ # @return [Array<String>] The RBS lines, formatted as specified.
42
+ def generate_rbs(indent_level, options); end
43
+
44
+ sig do
45
+ abstract.params(
46
+ others: T::Array[RbsGenerator::RbsObject]
47
+ ).returns(T::Boolean)
48
+ end
49
+ # Given an array of other objects, returns true if they may be merged
50
+ # into this instance using {merge_into_self}. Each subclass will have its
51
+ # own criteria on what allows objects to be mergeable.
52
+ #
53
+ # @abstract
54
+ # @param others [Array<RbsGenerator::RbsObject>] An array of other {RbsObject} instances.
55
+ # @return [Boolean] Whether this instance may be merged with them.
56
+ def mergeable?(others); end
57
+
58
+ sig do
59
+ abstract.params(
60
+ others: T::Array[RbsGenerator::RbsObject]
61
+ ).void
62
+ end
63
+ # Given an array of other objects, merges them into this one. Each
64
+ # subclass will do this differently.
65
+ # You MUST ensure that {mergeable?} is true for those instances.
66
+ #
67
+ # @abstract
68
+ # @param others [Array<RbsGenerator::RbsObject>] An array of other {RbsObject} instances.
69
+ # @return [void]
70
+ def merge_into_self(others); end
71
+
72
+ sig { overridable.override.returns(String) }
73
+ def describe
74
+ 'RBS object'
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,96 @@
1
+ # typed: true
2
+ module Parlour
3
+ class RbsGenerator < Generator
4
+ # Represents a type alias.
5
+ class TypeAlias < RbsObject
6
+ sig do
7
+ params(
8
+ generator: Generator,
9
+ name: String,
10
+ type: Types::TypeLike,
11
+ block: T.nilable(T.proc.params(x: TypeAlias).void)
12
+ ).void
13
+ end
14
+ # Creates a new type alias.
15
+ #
16
+ # @param name [String] The name of the alias.
17
+ # @param value [String] The type to alias to.
18
+ def initialize(generator, name:, type:, &block)
19
+ super(generator, name)
20
+ @type = type
21
+ yield_self(&block) if block
22
+ end
23
+
24
+ # @return [String] The type to alias to.
25
+ sig { returns(Types::TypeLike) }
26
+ attr_reader :type
27
+
28
+ sig { params(other: Object).returns(T::Boolean) }
29
+ # Returns true if this instance is equal to another type alias.
30
+ #
31
+ # @param other [Object] The other instance. If this is not a {TypeAlias} (or a
32
+ # subclass of it), this will always return false.
33
+ # @return [Boolean]
34
+ def ==(other)
35
+ TypeAlias === other && name == other.name && type == other.type
36
+ end
37
+
38
+ sig do
39
+ override.params(
40
+ indent_level: Integer,
41
+ options: Options
42
+ ).returns(T::Array[String])
43
+ end
44
+ # Generates the RBS lines for this type alias.
45
+ #
46
+ # @param indent_level [Integer] The indentation level to generate the lines at.
47
+ # @param options [Options] The formatting options to use.
48
+ # @return [Array<String>] The RBS lines, formatted as specified.
49
+ def generate_rbs(indent_level, options)
50
+ [options.indented(indent_level,
51
+ "type #{name} = #{String === @type ? @type : @type.generate_rbs}"
52
+ )]
53
+ end
54
+
55
+ sig do
56
+ override.params(
57
+ others: T::Array[RbsGenerator::RbsObject]
58
+ ).returns(T::Boolean)
59
+ end
60
+ # Given an array of {TypeAlias} instances, returns true if they may be
61
+ # merged into this instance using {merge_into_self}. This is always false.
62
+ #
63
+ # @param others [Array<RbiGenerator::RbsObject>] An array of other
64
+ # {TypeAlias} instances.
65
+ # @return [Boolean] Whether this instance may be merged with them.
66
+ def mergeable?(others)
67
+ others.all? { |other| self == other }
68
+ end
69
+
70
+ sig do
71
+ override.params(
72
+ others: T::Array[RbsGenerator::RbsObject]
73
+ ).void
74
+ end
75
+ # Given an array of {TypeAlias} instances, merges them into this one.
76
+ # This particular implementation will simply do nothing, as instances
77
+ # are only mergeable if they are indentical.
78
+ # You MUST ensure that {mergeable?} is true for those instances.
79
+ #
80
+ # @param others [Array<RbiGenerator::RbsObject>] An array of other
81
+ # {TypeAlias} instances.
82
+ # @return [void]
83
+ def merge_into_self(others)
84
+ # We don't need to change anything! We only merge identical type alias
85
+ end
86
+
87
+ sig { override.returns(String) }
88
+ # Returns a human-readable brief string description of this code.
89
+ #
90
+ # @return [String]
91
+ def describe
92
+ "Type Alias (#{name} = #{type})"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -10,27 +10,34 @@ module Parlour
10
10
  # TODO: make this into a class which stores configuration and passes it to
11
11
  # all typeparsers
12
12
 
13
- sig { params(source: String, filename: T.nilable(String)).returns(RbiGenerator::Namespace) }
13
+ sig { params(source: String, filename: T.nilable(String), generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
14
14
  # Converts Ruby source code into a tree of objects.
15
15
  #
16
16
  # @param [String] source The Ruby source code.
17
17
  # @param [String, nil] filename The filename to use when parsing this code.
18
18
  # This may be used in error messages, but is optional.
19
19
  # @return [RbiGenerator::Namespace] The root of the object tree.
20
- def self.load_source(source, filename = nil)
21
- TypeParser.from_source(filename || '(source)', source).parse_all
20
+ def self.load_source(source, filename = nil, generator: nil)
21
+ TypeParser.from_source(filename || '(source)', source, generator: generator).parse_all
22
22
  end
23
23
 
24
- sig { params(filename: String).returns(RbiGenerator::Namespace) }
24
+ sig { params(filename: String, generator: T.nilable(RbiGenerator)).returns(RbiGenerator::Namespace) }
25
25
  # Converts Ruby source code into a tree of objects from a file.
26
26
  #
27
27
  # @param [String] filename The name of the file to load code from.
28
28
  # @return [RbiGenerator::Namespace] The root of the object tree.
29
- def self.load_file(filename)
30
- load_source(File.read(filename), filename)
29
+ def self.load_file(filename, generator: nil)
30
+ load_source(File.read(filename), filename, generator: generator)
31
+ end
32
+
33
+ sig do
34
+ params(
35
+ root: String,
36
+ inclusions: T::Array[String],
37
+ exclusions: T::Array[String],
38
+ generator: T.nilable(RbiGenerator),
39
+ ).returns(RbiGenerator::Namespace)
31
40
  end
32
-
33
- sig { params(root: String).returns(RbiGenerator::Namespace) }
34
41
  # Loads an entire Sorbet project using Sorbet's file table, obeying any
35
42
  # "typed: ignore" sigils, into a tree of objects.
36
43
  #
@@ -39,8 +46,15 @@ module Parlour
39
46
  #
40
47
  # @param [String] root The root of the project; where the "sorbet" directory
41
48
  # and "Gemfile" are located.
49
+ # @param [Array<String>] inclusions A list of files to include when loading
50
+ # the project, relative to the given root.
51
+ # @param [Array<String>] exclusions A list of files to exclude when loading
52
+ # the project, relative to the given root.
42
53
  # @return [RbiGenerator::Namespace] The root of the object tree.
43
- def self.load_project(root)
54
+ def self.load_project(root, inclusions: ['.'], exclusions: [], generator: nil)
55
+ expanded_inclusions = inclusions.map { |i| File.expand_path(i, root) }
56
+ expanded_exclusions = exclusions.map { |e| File.expand_path(e, root) }
57
+
44
58
  stdin, stdout, stderr, wait_thr = T.unsafe(Open3).popen3(
45
59
  'bundle exec srb tc -p file-table-json',
46
60
  chdir: root
@@ -58,12 +72,18 @@ module Parlour
58
72
  next if rel_path.start_with?('./sorbet/rbi/hidden-definitions/')
59
73
  path = File.expand_path(rel_path, root)
60
74
 
75
+ # Skip this file if it was excluded
76
+ next if !expanded_inclusions.any? { |i| path.start_with?(i) } \
77
+ || expanded_exclusions.any? { |e| path.start_with?(e) }
78
+
61
79
  # There are some entries which are URLs to stdlib
62
80
  next unless File.exist?(path)
63
81
 
64
- namespaces << load_file(path)
82
+ namespaces << load_file(path, generator: generator)
65
83
  end
66
84
 
85
+ namespaces.uniq!
86
+
67
87
  raise 'project is empty' if namespaces.empty?
68
88
 
69
89
  first_namespace, *other_namespaces = namespaces
@@ -83,7 +83,7 @@ module Parlour
83
83
 
84
84
  extend T::Sig
85
85
 
86
- sig { params(ast: Parser::AST::Node, unknown_node_errors: T::Boolean).void }
86
+ sig { params(ast: Parser::AST::Node, unknown_node_errors: T::Boolean, generator: T.nilable(RbiGenerator)).void }
87
87
  # Creates a new {TypeParser} from whitequark/parser AST.
88
88
  #
89
89
  # @param [Parser::AST::Node] The AST.
@@ -92,23 +92,26 @@ module Parlour
92
92
  # if true, a parse error is raised. Setting this to true is likely to
93
93
  # raise errors for lots of non-RBI Ruby code, but setting it to false
94
94
  # could miss genuine typed objects if Parlour or your code contains a bug.
95
- def initialize(ast, unknown_node_errors: false)
95
+ def initialize(ast, unknown_node_errors: false, generator: nil)
96
96
  @ast = ast
97
97
  @unknown_node_errors = unknown_node_errors
98
+ @generator = generator || DetachedRbiGenerator.new
98
99
  end
99
100
 
100
- sig { params(filename: String, source: String).returns(TypeParser) }
101
+ sig { params(filename: String, source: String, generator: T.nilable(RbiGenerator)).returns(TypeParser) }
101
102
  # Creates a new {TypeParser} from a source file and its filename.
102
103
  #
103
104
  # @param [String] filename A filename. This does not need to be an actual
104
105
  # file; it merely identifies this source.
105
106
  # @param [String] source The Ruby source code.
106
107
  # @return [TypeParser]
107
- def self.from_source(filename, source)
108
+ def self.from_source(filename, source, generator: nil)
108
109
  buffer = Parser::Source::Buffer.new(filename)
109
110
  buffer.source = source
110
-
111
- TypeParser.new(Parser::CurrentRuby.new.parse(buffer))
111
+
112
+ # || special case handles parser returning nil on an empty file
113
+ parsed = Parser::CurrentRuby.new.parse(buffer) || Parser::AST::Node.new(:body)
114
+ TypeParser.new(parsed, generator: generator)
112
115
  end
113
116
 
114
117
  sig { returns(Parser::AST::Node) }
@@ -117,15 +120,19 @@ module Parlour
117
120
 
118
121
  sig { returns(T::Boolean) }
119
122
  # @return [Boolean] Whether to raise an error if a node of an unknown kind
120
- # is encountered.
123
+ # is encountered.
121
124
  attr_reader :unknown_node_errors
122
125
 
126
+ sig { returns(RbiGenerator) }
127
+ # @return [RbiGenerator] The {RbiGenerator} to load the source into.
128
+ attr_accessor :generator
129
+
123
130
  # Parses the entire source file and returns the resulting root namespace.
124
131
  #
125
132
  # @return [RbiGenerator::Namespace] The root namespace of the parsed source.
126
133
  sig { returns(RbiGenerator::Namespace) }
127
134
  def parse_all
128
- root = RbiGenerator::Namespace.new(DetachedRbiGenerator.new)
135
+ root = generator.root
129
136
  root.children.concat(parse_path_to_object(NodePath.new([])))
130
137
  root
131
138
  end
@@ -134,7 +141,7 @@ module Parlour
134
141
  # represents and returns it, recursing to any child namespaces and parsing
135
142
  # any methods within.
136
143
  #
137
- # If the node directly represents several nodes, such as being a
144
+ # If the node directly represents several nodes, such as being a
138
145
  # (begin ...) node, they are all returned.
139
146
  #
140
147
  # @param [NodePath] path The path to the namespace definition. Do not pass
@@ -144,7 +151,7 @@ module Parlour
144
151
  sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::RbiObject]) }
145
152
  def parse_path_to_object(path, is_within_eigenclass: false)
146
153
  node = path.traverse(ast)
147
-
154
+
148
155
  case node.type
149
156
  when :class
150
157
  parse_err 'cannot declare classes in an eigenclass', node if is_within_eigenclass
@@ -158,9 +165,9 @@ module Parlour
158
165
  *parent_names, this_name = constant_names(name)
159
166
  target = T.let(nil, T.nilable(RbiGenerator::Namespace))
160
167
  top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
161
- parent_names.each do |n|
168
+ parent_names.each do |n|
162
169
  new_obj = RbiGenerator::Namespace.new(
163
- DetachedRbiGenerator.new,
170
+ generator,
164
171
  n.to_s,
165
172
  false,
166
173
  )
@@ -169,18 +176,103 @@ module Parlour
169
176
  top_level ||= new_obj
170
177
  end if parent_names
171
178
 
172
- final_obj = RbiGenerator::ClassNamespace.new(
173
- DetachedRbiGenerator.new,
174
- this_name.to_s,
175
- final,
176
- node_to_s(superclass),
177
- abstract,
178
- ) do |c|
179
- c.children.concat(parse_path_to_object(path.child(2))) if body
180
- c.create_includes(includes)
181
- c.create_extends(extends)
179
+ # Instantiate the correct kind of class
180
+ if ['T::Struct', '::T::Struct'].include?(node_to_s(superclass))
181
+ # Find all of this struct's props and consts
182
+ # The body is typically a `begin` element but when there's only
183
+ # one node there's no wrapping block and instead it would directly
184
+ # be the node.
185
+ prop_nodes = body.nil? ? [] :
186
+ (body.type == :begin ? body.to_a : [body]).select { |x| x.type == :send && [:prop, :const].include?(x.to_a[1]) }
187
+
188
+ props = prop_nodes.map do |prop_node|
189
+ _, prop_type, name_node, type_node, extras_hash_node = *prop_node
190
+
191
+ # "const" is just "prop ..., immutable: true"
192
+ extras_hash = extras_hash_node.to_a.map do |pair_node|
193
+ key_node, value_node = *pair_node
194
+ parse_err 'prop/const key must be a symbol', prop_node unless key_node.type == :sym
195
+ key = key_node.to_a.first
196
+
197
+ value =
198
+ if key == :default
199
+ T.must(node_to_s(value_node))
200
+ else
201
+ case value_node.type
202
+ when :true
203
+ true
204
+ when :false
205
+ false
206
+ when :sym
207
+ value_node.to_a.first
208
+ else
209
+ T.must(node_to_s(value_node))
210
+ end
211
+ end
212
+
213
+ [key, value]
214
+ end.to_h
215
+
216
+ if prop_type == :const
217
+ parse_err 'const cannot use immutable key', prop_node unless extras_hash[:immutable].nil?
218
+ extras_hash[:immutable] = true
219
+ end
220
+
221
+ # Get prop/const name
222
+ parse_err 'prop/const name must be a symbol or string', prop_node unless [:sym, :str].include?(name_node.type)
223
+ name = name_node.to_a.first.to_s
224
+
225
+ RbiGenerator::StructProp.new(
226
+ name,
227
+ T.must(node_to_s(type_node)),
228
+ **T.unsafe(extras_hash)
229
+ )
230
+ end
231
+
232
+ final_obj = RbiGenerator::StructClassNamespace.new(
233
+ generator,
234
+ this_name.to_s,
235
+ final,
236
+ props,
237
+ abstract,
238
+ )
239
+ elsif ['T::Enum', '::T::Enum'].include?(node_to_s(superclass))
240
+ # Look for (block (send nil :enums) ...) structure
241
+ enums_node = body.nil? ? nil :
242
+ (body.type == :begin ? body.to_a : [body]).find { |x| x.type == :block && x.to_a[0].type == :send && x.to_a[0].to_a[1] == :enums }
243
+
244
+ # Find the constant assigments within this block
245
+ constant_nodes = enums_node.to_a[2].to_a
246
+
247
+ # Convert this to an array to enums as EnumClassNamespace expects
248
+ enums = constant_nodes.map do |constant_node|
249
+ _, name, new_node = *constant_node
250
+ serialize_value = node_to_s(new_node.to_a[2])
251
+
252
+ serialize_value ? [name.to_s, serialize_value] : name.to_s
253
+ end
254
+
255
+ final_obj = RbiGenerator::EnumClassNamespace.new(
256
+ generator,
257
+ this_name.to_s,
258
+ final,
259
+ enums,
260
+ abstract,
261
+ )
262
+ else
263
+ final_obj = RbiGenerator::ClassNamespace.new(
264
+ generator,
265
+ this_name.to_s,
266
+ final,
267
+ node_to_s(superclass),
268
+ abstract,
269
+ )
182
270
  end
183
271
 
272
+ final_obj.children.concat(parse_path_to_object(path.child(2))) if body
273
+ final_obj.create_includes(includes)
274
+ final_obj.create_extends(extends)
275
+
184
276
  if target
185
277
  target.children << final_obj
186
278
  [top_level]
@@ -199,9 +291,9 @@ module Parlour
199
291
  *parent_names, this_name = constant_names(name)
200
292
  target = T.let(nil, T.nilable(RbiGenerator::Namespace))
201
293
  top_level = T.let(nil, T.nilable(RbiGenerator::Namespace))
202
- parent_names.each do |n|
294
+ parent_names.each do |n|
203
295
  new_obj = RbiGenerator::Namespace.new(
204
- DetachedRbiGenerator.new,
296
+ generator,
205
297
  n.to_s,
206
298
  false,
207
299
  )
@@ -211,7 +303,7 @@ module Parlour
211
303
  end if parent_names
212
304
 
213
305
  final_obj = RbiGenerator::ModuleNamespace.new(
214
- DetachedRbiGenerator.new,
306
+ generator,
215
307
  this_name.to_s,
216
308
  final,
217
309
  interface,
@@ -230,24 +322,34 @@ module Parlour
230
322
  when :send, :block
231
323
  if sig_node?(node)
232
324
  parse_sig_into_methods(path, is_within_eigenclass: is_within_eigenclass)
325
+ elsif node.type == :send &&
326
+ [:attr_reader, :attr_writer, :attr_accessor].include?(node.to_a[1]) &&
327
+ !previous_sibling_sig_node?(path)
328
+ parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
233
329
  else
234
330
  []
235
331
  end
236
332
  when :def, :defs
237
- # TODO: Support for defs without sigs
238
- # If so, we need some kind of state machine to determine whether
239
- # they've already been dealt with by the "when :send" clause and
240
- # #parse_sig_into_methods.
241
- # If not, just ignore this.
242
- []
333
+ if previous_sibling_sig_node?(path)
334
+ []
335
+ else
336
+ parse_method_into_methods(path, is_within_eigenclass: is_within_eigenclass)
337
+ end
243
338
  when :sclass
244
339
  parse_err 'cannot access eigen of non-self object', node unless node.to_a[0].type == :self
245
340
  parse_path_to_object(path.child(1), is_within_eigenclass: true)
246
341
  when :begin
247
342
  # Just map over all the things
248
343
  node.to_a.length.times.map do |c|
249
- parse_path_to_object(path.child(c), is_within_eigenclass: is_within_eigenclass)
344
+ parse_path_to_object(path.child(c), is_within_eigenclass: is_within_eigenclass)
250
345
  end.flatten
346
+ when :casgn
347
+ _, name, body = *node
348
+ [Parlour::RbiGenerator::Constant.new(
349
+ generator,
350
+ name: T.must(name).to_s,
351
+ value: T.must(node_to_s(body)),
352
+ )]
251
353
  else
252
354
  if unknown_node_errors
253
355
  parse_err "don't understand node type #{node.type}", node
@@ -259,6 +361,7 @@ module Parlour
259
361
 
260
362
  # A parsed sig, not associated with a method.
261
363
  class IntermediateSig < T::Struct
364
+ prop :type_parameters, T.nilable(T::Array[Symbol])
262
365
  prop :overridable, T::Boolean
263
366
  prop :override, T::Boolean
264
367
  prop :abstract, T::Boolean
@@ -318,7 +421,18 @@ module Parlour
318
421
  arg.to_a
319
422
  end
320
423
 
424
+ # Find type parameters if they were used
425
+ type_parameters = sig_chain
426
+ .find { |(n, _)| n == :type_parameters }
427
+ &.then do |(_, a)|
428
+ a.map do |arg|
429
+ parse_err 'type parameter must be a symbol', arg if arg.type != :sym
430
+ arg.to_a[0]
431
+ end
432
+ end
433
+
321
434
  IntermediateSig.new(
435
+ type_parameters: type_parameters,
322
436
  overridable: overridable,
323
437
  override: override,
324
438
  abstract: abstract,
@@ -370,7 +484,7 @@ module Parlour
370
484
  || target != nil
371
485
 
372
486
  parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
373
-
487
+
374
488
  kind = :attr
375
489
  attr_direction = method_name.to_s.gsub('attr_', '').to_sym
376
490
  def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
@@ -389,6 +503,13 @@ module Parlour
389
503
  return_type = this_sig.return_type
390
504
 
391
505
  if kind == :def
506
+ # Sorbet allows a trailing blockarg that's not in the sig
507
+ if params &&
508
+ def_params.length == params.length + 1 &&
509
+ def_params[-1].type == :blockarg
510
+ def_params = def_params[0...-1]
511
+ end
512
+
392
513
  parse_err 'mismatching number of arguments in sig and def', sig_block_node \
393
514
  if params && def_params.length != params.length
394
515
 
@@ -399,6 +520,7 @@ module Parlour
399
520
  parameters = params \
400
521
  ? zip_by(params, ->x{ x.to_a[0].to_a[0] }, def_params, ->x{ x.to_a[0] })
401
522
  .map do |sig_arg, def_param|
523
+
402
524
  arg_name = def_param.to_a[0]
403
525
 
404
526
  # TODO: anonymous restarg
@@ -418,10 +540,11 @@ module Parlour
418
540
  # There should only be one ever here, but future-proofing anyway
419
541
  def_names.map do |def_name|
420
542
  RbiGenerator::Method.new(
421
- DetachedRbiGenerator.new,
543
+ generator,
422
544
  def_name,
423
545
  parameters,
424
546
  return_type,
547
+ type_parameters: this_sig.type_parameters,
425
548
  override: this_sig.override,
426
549
  overridable: this_sig.overridable,
427
550
  abstract: this_sig.abstract,
@@ -437,7 +560,7 @@ module Parlour
437
560
 
438
561
  parse_err "attr_#{attr_direction} sig should have non-void return", sig_block_node \
439
562
  unless return_type
440
-
563
+
441
564
  attr_type = return_type
442
565
  when :writer
443
566
  # These are special and can only have one name
@@ -457,7 +580,111 @@ module Parlour
457
580
 
458
581
  def_names.map do |def_name|
459
582
  RbiGenerator::Attribute.new(
460
- DetachedRbiGenerator.new,
583
+ generator,
584
+ def_name,
585
+ attr_direction,
586
+ attr_type,
587
+ class_attribute: class_method
588
+ )
589
+ end
590
+ else
591
+ raise "unknown definition kind #{kind}"
592
+ end
593
+ end
594
+
595
+ sig { params(path: NodePath, is_within_eigenclass: T::Boolean).returns(T::Array[RbiGenerator::Method]) }
596
+ # Given a path to a method in the AST, finds the associated definition and
597
+ # parses them into methods.
598
+ # Usually this will return one method; the only exception currently is for
599
+ # attributes, where multiple can be declared in one call, e.g.
600
+ # +attr_reader :x, :y, :z+.
601
+ #
602
+ # @param [NodePath] path The sig to parse.
603
+ # @param [Boolean] is_within_eigenclass Whether the method definition this sig is
604
+ # associated with appears inside an eigenclass definition. If true, the
605
+ # returned method is made a class method. If the method definition
606
+ # is already a class method, an exception is thrown as the method will be
607
+ # a class method of the eigenclass, which Parlour can't represent.
608
+ # @return [<RbiGenerator::Method>] The parsed methods.
609
+ def parse_method_into_methods(path, is_within_eigenclass: false)
610
+ # A :def node represents a definition like "def x; end"
611
+ # A :defs node represents a definition like "def self.x; end"
612
+ def_node = path.traverse(ast)
613
+ case def_node.type
614
+ when :def
615
+ class_method = false
616
+ def_names = [def_node.to_a[0].to_s]
617
+ def_params = def_node.to_a[1].to_a
618
+ kind = :def
619
+ when :defs
620
+ parse_err 'targeted definitions on a non-self target are not supported', def_node \
621
+ unless def_node.to_a[0].type == :self
622
+ class_method = true
623
+ def_names = [def_node.to_a[1].to_s]
624
+ def_params = def_node.to_a[2].to_a
625
+ kind = :def
626
+ when :send
627
+ target, method_name, *parameters = *def_node
628
+
629
+ parse_err 'node after a sig must be a method definition', def_node \
630
+ unless [:attr_reader, :attr_writer, :attr_accessor].include?(method_name) \
631
+ || target != nil
632
+
633
+ parse_err 'typed attribute should have at least one name', def_node if parameters&.length == 0
634
+
635
+ kind = :attr
636
+ attr_direction = method_name.to_s.gsub('attr_', '').to_sym
637
+ def_names = T.must(parameters).map { |param| param.to_a[0].to_s }
638
+ class_method = false
639
+ else
640
+ parse_err 'node after a sig must be a method definition', def_node
641
+ end
642
+
643
+ if is_within_eigenclass
644
+ parse_err 'cannot represent multiple levels of eigenclassing', def_node if class_method
645
+ class_method = true
646
+ end
647
+
648
+ return_type = "T.untyped"
649
+
650
+ if kind == :def
651
+ parameters = def_params.map do |def_param|
652
+ arg_name = def_param.to_a[0]
653
+
654
+ # TODO: anonymous restarg
655
+ full_name = arg_name.to_s
656
+ full_name = "*#{arg_name}" if def_param.type == :restarg
657
+ full_name = "**#{arg_name}" if def_param.type == :kwrestarg
658
+ full_name = "#{arg_name}:" if def_param.type == :kwarg || def_param.type == :kwoptarg
659
+ full_name = "&#{arg_name}" if def_param.type == :blockarg
660
+
661
+ default = def_param.to_a[1] ? node_to_s(def_param.to_a[1]) : nil
662
+ type = nil
663
+
664
+ RbiGenerator::Parameter.new(full_name, type: type, default: default)
665
+ end
666
+
667
+ # There should only be one ever here, but future-proofing anyway
668
+ def_names.map do |def_name|
669
+ RbiGenerator::Method.new(
670
+ generator,
671
+ def_name,
672
+ parameters,
673
+ return_type,
674
+ class_method: class_method
675
+ )
676
+ end
677
+ elsif kind == :attr
678
+ case attr_direction
679
+ when :reader, :accessor, :writer
680
+ attr_type = return_type
681
+ else
682
+ raise "unknown attribute direction #{attr_direction}"
683
+ end
684
+
685
+ def_names.map do |def_name|
686
+ RbiGenerator::Attribute.new(
687
+ generator,
461
688
  def_name,
462
689
  attr_direction,
463
690
  attr_type,
@@ -469,14 +696,166 @@ module Parlour
469
696
  end
470
697
  end
471
698
 
699
+ sig { params(str: String).returns(Types::Type) }
700
+ # TODO doc
701
+ def self.parse_single_type(str)
702
+ i = TypeParser.from_source('(none)', str)
703
+ i.parse_node_to_type(i.ast)
704
+ end
705
+
706
+ sig { params(node: Parser::AST::Node).returns(Types::Type) }
707
+ # Given an AST node representing an RBI type (such as 'T::Array[String]'),
708
+ # parses it into a generic type.
709
+ #
710
+ # @param [Parser::AST::Node] node
711
+ # @return [Parlour::Types::Type]
712
+ def parse_node_to_type(node)
713
+ case node.type
714
+ when :send
715
+ target, message, *args = *node
716
+
717
+ # Special case: is this a generic type instantiation?
718
+ if message == :[]
719
+ names = constant_names(target)
720
+ known_single_element_collections = [:Array, :Set, :Range, :Enumerator, :Enumerable]
721
+
722
+ if names.length == 2 && names[0] == :T &&
723
+ known_single_element_collections.include?(names[1])
724
+
725
+ parse_err "no type in T::#{names[1]}[...]", node if args.nil? || args.empty?
726
+ parse_err "too many types in T::#{names[1]}[...]", node unless args.length == 1
727
+ return T.must(Types.const_get(T.must(names[1]))).new(parse_node_to_type(T.must(args.first)))
728
+ elsif names.length == 2 && names == [:T, :Hash]
729
+ parse_err "not enough types in T::Hash[...]", node if args.nil? || args.length < 2
730
+ parse_err "too many types in T::Hash[...]", node unless args.length == 2
731
+ return Types::Hash.new(
732
+ parse_node_to_type(args[0]), parse_node_to_type(args[1])
733
+ )
734
+ else
735
+ # TODO
736
+ warning "user-defined generic types not implemented, treating #{names.last} as untyped", node
737
+ return Types::Untyped.new
738
+ end
739
+ end
740
+
741
+ # Special case: is this a proc?
742
+ # This parsing is pretty simplified, but you'd also have to be doing
743
+ # something pretty cursed with procs to break this
744
+ # This checks for (send (send (send (const nil :T) :proc) ...) ...)
745
+ # That's the right amount of nesting for T.proc.params(...).returns(...)
746
+ if node.to_a[0].type == :send &&
747
+ node.to_a[0].to_a[0].type == :send &&
748
+ node.to_a[0].to_a[0].to_a[1] == :proc &&
749
+ node.to_a[0].to_a[0].to_a[0].type == :const &&
750
+ node.to_a[0].to_a[0].to_a[0].to_a == [nil, :T] # yuck
751
+
752
+ # Get parameters
753
+ params_send = node.to_a[0]
754
+ parse_err "expected 'params' to follow 'T.proc'", node unless params_send.to_a[1] == :params
755
+ parse_err "expected 'params' to have kwargs", node unless params_send.to_a[2].type == :hash
756
+
757
+ parameters = params_send.to_a[2].to_a.map do |pair|
758
+ name, value = *pair
759
+ parse_err "expected 'params' name to be symbol", node unless name.type == :sym
760
+ name = name.to_a[0].to_s
761
+ value = parse_node_to_type(value)
762
+
763
+ RbiGenerator::Parameter.new(name, type: value)
764
+ end
765
+
766
+ # Get return value
767
+ if node.to_a[1] == :void
768
+ return_type = nil
769
+ else
770
+ _, call, *args = *node
771
+ parse_err 'expected .returns or .void', node unless call == :returns
772
+ parse_err 'no argument to .returns', node if args.nil? || args.empty?
773
+ parse_err 'too many arguments to .returns', node unless args.length == 1
774
+ return_type = parse_node_to_type(T.must(args.first))
775
+ end
776
+
777
+ return Types::Proc.new(parameters, return_type)
778
+ end
779
+
780
+ # The other options for a valid call are all "T.something" methods
781
+ parse_err "unexpected call #{node_to_s(node).inspect} in type", node \
782
+ unless target.type == :const && target.to_a == [nil, :T]
783
+
784
+ case message
785
+ when :nilable
786
+ parse_err 'no argument to T.nilable', node if args.nil? || args.empty?
787
+ parse_err 'too many arguments to T.nilable', node unless args.length == 1
788
+ Types::Nilable.new(parse_node_to_type(T.must(args.first)))
789
+ when :any
790
+ Types::Union.new((args || []).map { |x| parse_node_to_type(T.must(x)) })
791
+ when :all
792
+ Types::Intersection.new((args || []).map { |x| parse_node_to_type(T.must(x)) })
793
+ when :let
794
+ # Not really allowed in a type signature, but handy for generalizing
795
+ # constant types
796
+ parse_err 'not enough argument to T.let', node if args.nil? || args.length < 2
797
+ parse_err 'too many arguments to T.nilable', node unless args.length == 2
798
+ parse_node_to_type(args[1])
799
+ when :type_parameter
800
+ parse_err 'no argument to T.type_parameter', node if args.nil? || args.empty?
801
+ parse_err 'too many arguments to T.type_parameter', node unless args.length == 1
802
+ parse_err 'expected T.type_parameter to be passed a symbol', node unless T.must(args.first).type == :sym
803
+ Types::Raw.new(T.must(args.first.to_a[0].to_s))
804
+ when :class_of
805
+ parse_err 'no argument to T.class_of', node if args.nil? || args.empty?
806
+ parse_err 'too many arguments to T.class_of', node unless args.length == 1
807
+ Types::Class.new(parse_node_to_type(args[0]))
808
+ when :untyped
809
+ parse_err 'T.untyped does not accept arguments', node if !args.nil? && !args.empty?
810
+ Types::Untyped.new
811
+ else
812
+ warning "unknown method T.#{message}, treating as untyped", node
813
+ Types::Untyped.new
814
+ end
815
+ when :const
816
+ # Special case: T::Boolean
817
+ if constant_names(node) == [:T, :Boolean]
818
+ return Types::Boolean.new
819
+ end
820
+
821
+ # Otherwise, just a plain old constant
822
+ Types::Raw.new(constant_names(node).join('::'))
823
+ when :array
824
+ # Tuple
825
+ Types::Tuple.new(node.to_a.map { |x| parse_node_to_type(T.must(x)) })
826
+ when :hash
827
+ # Shape/record
828
+ keys_to_types = node.to_a.map do |pair|
829
+ key, value = *pair
830
+ parse_err "all shape keys must be symbols", node unless key.type == :sym
831
+ [key.to_a[0], parse_node_to_type(value)]
832
+ end.to_h
833
+
834
+ Types::Record.new(keys_to_types)
835
+ else
836
+ parse_err "unable to parse type #{node_to_s(node).inspect}", node
837
+ end
838
+ end
839
+
472
840
  protected
473
841
 
842
+ sig { params(msg: String, node: Parser::AST::Node).void }
843
+ def warning(msg, node)
844
+ return if $VERBOSE.nil?
845
+
846
+ print Rainbow("Parlour warning: ").yellow.dark.bold
847
+ print Rainbow("Type generalization: ").magenta.bright.bold
848
+ puts msg
849
+ print Rainbow(" └ at code: ").blue.bright.bold
850
+ puts node_to_s(node)
851
+ end
852
+
474
853
  sig { params(node: T.nilable(Parser::AST::Node)).returns(T::Array[Symbol]) }
475
854
  # Given a node representing a simple chain of constants (such as A or
476
855
  # A::B::C), converts that node into an array of the constant names which
477
856
  # are accessed. For example, A::B::C would become [:A, :B, :C].
478
857
  #
479
- # @param [Parser::AST::Node, nil] node The node to convert. This must
858
+ # @param [Parser::AST::Node, nil] node The node to convert. This must
480
859
  # consist only of nested (:const) nodes.
481
860
  # @return [Array<Symbol>] The chain of constant names.
482
861
  def constant_names(node)
@@ -497,6 +876,24 @@ module Parlour
497
876
  node.to_a[0].to_a[1] == :sig
498
877
  end
499
878
 
879
+ sig { params(path: NodePath).returns(T::Boolean) }
880
+ # Given a path, returns a boolean indicating whether the previous sibling
881
+ # represents a call to "sig" with a block.
882
+ #
883
+ # @param [NodePath] path The path to the namespace definition.
884
+ # @return [Boolean] True if that node represents a "sig" call, false
885
+ # otherwise.
886
+ def previous_sibling_sig_node?(path)
887
+ previous_sibling = path.sibling(-1)
888
+ previous_node = previous_sibling.traverse(ast)
889
+ sig_node?(previous_node)
890
+ rescue IndexError, ArgumentError, TypeError
891
+ # `sibling` call could raise IndexError or ArgumentError if reaching into negative indices
892
+ # `traverse` call could raise TypeError if path doesn't return Parser::AST::Node
893
+
894
+ false
895
+ end
896
+
500
897
  sig { params(node: T.nilable(Parser::AST::Node)).returns(T.nilable(String)) }
501
898
  # Given an AST node, returns the source code from which it was constructed.
502
899
  # If the given AST node is nil, this returns nil.
@@ -524,13 +921,13 @@ module Parlour
524
921
  def body_has_modifier?(node, modifier)
525
922
  return false unless node
526
923
 
527
- (node.type == :send && node.to_a == [nil, modifier]) ||
924
+ (node.type == :send && node.to_a == [nil, modifier]) ||
528
925
  (node.type == :begin &&
529
926
  node.to_a.any? { |c| c.type == :send && c.to_a == [nil, modifier] })
530
927
  end
531
928
 
532
929
  sig { params(node: Parser::AST::Node).returns([T::Array[String], T::Array[String]]) }
533
- # Given an AST node representing the body of a class or module, returns two
930
+ # Given an AST node representing the body of a class or module, returns two
534
931
  # arrays of the includes and extends contained within the body.
535
932
  #
536
933
  # @param [Parser::AST::Node] node The body of the namespace.
@@ -568,7 +965,7 @@ module Parlour
568
965
  raise ParseError.new(buffer, range), desc
569
966
  end
570
967
 
571
- sig do
968
+ sig do
572
969
  type_parameters(:A, :B)
573
970
  .params(
574
971
  a: T::Array[T.type_parameter(:A)],
@@ -580,7 +977,7 @@ module Parlour
580
977
  end
581
978
  # Given two arrays and functions to get a key for each item in the two
582
979
  # arrays, joins the two arrays into one array of pairs by that key.
583
- #
980
+ #
584
981
  # The arrays should both be the same length, and the key functions should
585
982
  # never return duplicate keys for two different items.
586
983
  #
@@ -588,7 +985,7 @@ module Parlour
588
985
  # @param [A -> Any] fa A function to obtain a key for any element in the
589
986
  # first array.
590
987
  # @param [Array<B>] b The second array.
591
- # @param [B -> Any] fb A function to obtain a key for any element in the
988
+ # @param [B -> Any] fb A function to obtain a key for any element in the
592
989
  # second array.
593
990
  # @return [Array<(A, B)>] An array of pairs, where the left of the pair is
594
991
  # an element from A and the right is the element from B with the
@@ -606,4 +1003,4 @@ module Parlour
606
1003
  end
607
1004
  end
608
1005
  end
609
- end
1006
+ end