spoom 1.6.3 → 1.7.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.
@@ -1,33 +1,5 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "sigils"
5
-
6
- module Spoom
7
- module Sorbet
8
- module MetricsParser
9
- DEFAULT_PREFIX = "ruby_typer.unknown."
10
-
11
- class << self
12
- #: (String path, ?String prefix) -> Hash[String, Integer]
13
- def parse_file(path, prefix = DEFAULT_PREFIX)
14
- parse_string(File.read(path), prefix)
15
- end
16
-
17
- #: (String string, ?String prefix) -> Hash[String, Integer]
18
- def parse_string(string, prefix = DEFAULT_PREFIX)
19
- parse_hash(JSON.parse(string), prefix)
20
- end
21
-
22
- #: (Hash[String, untyped] obj, ?String prefix) -> Hash[String, Integer]
23
- def parse_hash(obj, prefix = DEFAULT_PREFIX)
24
- obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
25
- name = metric["name"]
26
- name = name.sub(prefix, "")
27
- metrics[name] = metric["value"] || 0
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
4
+ require "spoom/sorbet/metrics/code_metrics_visitor"
5
+ require "spoom/sorbet/metrics/metrics_file_parser"
@@ -0,0 +1,239 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Translate
7
+ class RBSCommentsToSorbetSigs < Translator
8
+ include Spoom::RBS::ExtractRBSComments
9
+
10
+ # @override
11
+ #: (Prism::ClassNode node) -> void
12
+ def visit_class_node(node)
13
+ apply_class_annotations(node)
14
+
15
+ super
16
+ end
17
+
18
+ # @override
19
+ #: (Prism::ModuleNode node) -> void
20
+ def visit_module_node(node)
21
+ apply_class_annotations(node)
22
+
23
+ super
24
+ end
25
+
26
+ # @override
27
+ #: (Prism::SingletonClassNode node) -> void
28
+ def visit_singleton_class_node(node)
29
+ apply_class_annotations(node)
30
+
31
+ super
32
+ end
33
+
34
+ # @override
35
+ #: (Prism::DefNode node) -> void
36
+ def visit_def_node(node)
37
+ comments = node_rbs_comments(node)
38
+ return if comments.empty?
39
+
40
+ return if comments.signatures.empty?
41
+
42
+ builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
43
+ builder.visit(node)
44
+ rbi_node = builder.tree.nodes.first #: as RBI::Method
45
+
46
+ comments.signatures.each do |signature|
47
+ method_type = ::RBS::Parser.parse_method_type(signature.string)
48
+ translator = RBI::RBS::MethodTypeTranslator.new(rbi_node)
49
+ translator.visit(method_type)
50
+ sig = translator.result
51
+ apply_member_annotations(comments.annotations, sig)
52
+
53
+ @rewriter << Source::Replace.new(
54
+ signature.location.start_offset,
55
+ signature.location.end_offset,
56
+ sig.string,
57
+ )
58
+ rescue ::RBS::ParsingError
59
+ # Ignore signatures with errors
60
+ next
61
+ end
62
+ end
63
+
64
+ # @override
65
+ #: (Prism::CallNode node) -> void
66
+ def visit_call_node(node)
67
+ case node.message
68
+ when "attr_reader", "attr_writer", "attr_accessor"
69
+ visit_attr(node)
70
+ else
71
+ super
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ #: (Prism::CallNode) -> void
78
+ def visit_attr(node)
79
+ comments = node_rbs_comments(node)
80
+ return if comments.empty?
81
+
82
+ return if comments.signatures.empty?
83
+
84
+ comments.signatures.each do |signature|
85
+ attr_type = ::RBS::Parser.parse_type(signature.string)
86
+ sig = RBI::Sig.new
87
+
88
+ if node.message == "attr_writer"
89
+ if node.arguments&.arguments&.size != 1
90
+ raise Error, "AttrWriter must have exactly one name"
91
+ end
92
+
93
+ name = node.arguments&.arguments&.first #: as Prism::SymbolNode
94
+ sig.params << RBI::SigParam.new(
95
+ name.slice[1..-1], #: as String
96
+ RBI::RBS::TypeTranslator.translate(attr_type),
97
+ )
98
+ end
99
+
100
+ sig.return_type = RBI::RBS::TypeTranslator.translate(attr_type)
101
+
102
+ apply_member_annotations(comments.annotations, sig)
103
+
104
+ @rewriter << Source::Replace.new(
105
+ signature.location.start_offset,
106
+ signature.location.end_offset,
107
+ sig.string,
108
+ )
109
+ end
110
+ end
111
+
112
+ #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) -> void
113
+ def apply_class_annotations(node)
114
+ comments = node_rbs_comments(node)
115
+ return if comments.empty?
116
+
117
+ indent = " " * (node.location.start_column + 2)
118
+ insert_pos = case node
119
+ when Prism::ClassNode
120
+ (node.superclass || node.constant_path).location.end_offset
121
+ when Prism::ModuleNode
122
+ node.constant_path.location.end_offset
123
+ when Prism::SingletonClassNode
124
+ node.expression.location.end_offset
125
+ end
126
+
127
+ if comments.annotations.any?
128
+ unless already_extends?(node, /^(::)?T::Helpers$/)
129
+ @rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Helpers\n")
130
+ end
131
+
132
+ comments.annotations.reverse_each do |annotation|
133
+ from = adjust_to_line_start(annotation.location.start_offset)
134
+ to = adjust_to_line_end(annotation.location.end_offset)
135
+ @rewriter << Source::Delete.new(from, to)
136
+
137
+ content = case annotation.string
138
+ when "@abstract"
139
+ "abstract!"
140
+ when "@interface"
141
+ "interface!"
142
+ when "@sealed"
143
+ "sealed!"
144
+ when "@final"
145
+ "final!"
146
+ when /^@requires_ancestor: /
147
+ srb_type = ::RBS::Parser.parse_type(annotation.string.delete_prefix("@requires_ancestor: "))
148
+ rbs_type = RBI::RBS::TypeTranslator.translate(srb_type)
149
+ "requires_ancestor { #{rbs_type} }"
150
+ end
151
+
152
+ newline = node.body.nil? ? "" : "\n"
153
+ @rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{content}#{newline}")
154
+ end
155
+ end
156
+
157
+ signatures = comments.signatures
158
+ if signatures.any?
159
+ signatures.each do |signature|
160
+ type_params = ::RBS::Parser.parse_type_params(signature.string)
161
+ next if type_params.empty?
162
+
163
+ from = adjust_to_line_start(signature.location.start_offset)
164
+ to = adjust_to_line_end(signature.location.end_offset)
165
+ @rewriter << Source::Delete.new(from, to)
166
+
167
+ unless already_extends?(node, /^(::)?T::Generic$/)
168
+ @rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Generic\n")
169
+ end
170
+
171
+ type_params.each do |type_param|
172
+ type_member = "#{type_param.name} = type_member"
173
+
174
+ case type_param.variance
175
+ when :covariant
176
+ type_member = "#{type_member}(:out)"
177
+ when :contravariant
178
+ type_member = "#{type_member}(:in)"
179
+ end
180
+
181
+ if type_param.upper_bound || type_param.default_type
182
+ if type_param.upper_bound
183
+ rbs_type = RBI::RBS::TypeTranslator.translate(type_param.upper_bound)
184
+ type_member = "#{type_member} {{ upper: #{rbs_type} }}"
185
+ end
186
+
187
+ if type_param.default_type
188
+ rbs_type = RBI::RBS::TypeTranslator.translate(type_param.default_type)
189
+ type_member = "#{type_member} {{ fixed: #{rbs_type} }}"
190
+ end
191
+ end
192
+
193
+ newline = node.body.nil? ? "" : "\n"
194
+ @rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ #: (Array[RBS::Annotations], RBI::Sig) -> void
201
+ def apply_member_annotations(annotations, sig)
202
+ annotations.each do |annotation|
203
+ case annotation.string
204
+ when "@abstract"
205
+ sig.is_abstract = true
206
+ when "@final"
207
+ sig.is_final = true
208
+ when "@override"
209
+ sig.is_override = true
210
+ when "@override(allow_incompatible: true)"
211
+ sig.is_override = true
212
+ sig.allow_incompatible_override = true
213
+ when "@overridable"
214
+ sig.is_overridable = true
215
+ when "@without_runtime"
216
+ sig.without_runtime = true
217
+ end
218
+ end
219
+ end
220
+
221
+ #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode, Regexp) -> bool
222
+ def already_extends?(node, constant_regex)
223
+ node.child_nodes.any? do |c|
224
+ next false unless c.is_a?(Prism::CallNode)
225
+ next false unless c.message == "extend"
226
+ next false unless c.receiver.nil? || c.receiver.is_a?(Prism::SelfNode)
227
+ next false unless c.arguments&.arguments&.size == 1
228
+
229
+ arg = c.arguments&.arguments&.first
230
+ next false unless arg.is_a?(Prism::ConstantPathNode)
231
+ next false unless arg.slice.match?(constant_regex)
232
+
233
+ true
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,123 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Translate
7
+ # Translates Sorbet assertions to RBS comments.
8
+ class SorbetAssertionsToRBSComments < Translator
9
+ LINE_BREAK = "\n".ord #: Integer
10
+
11
+ # @override
12
+ #: (Prism::CallNode) -> void
13
+ def visit_call_node(node)
14
+ return super unless t_annotation?(node)
15
+ return super unless at_end_of_line?(node)
16
+
17
+ value = T.must(node.arguments&.arguments&.first)
18
+ rbs_annotation = build_rbs_annotation(node)
19
+
20
+ start_offset = node.location.start_offset
21
+ end_offset = node.location.end_offset
22
+ @rewriter << Source::Replace.new(start_offset, end_offset - 1, "#{dedent_value(node, value)} #{rbs_annotation}")
23
+ end
24
+
25
+ private
26
+
27
+ #: (Prism::CallNode) -> void
28
+ def build_rbs_annotation(call)
29
+ case call.name
30
+ when :let
31
+ srb_type = call.arguments&.arguments&.last #: as !nil
32
+ rbs_type = RBI::Type.parse_node(srb_type).rbs_string
33
+ "#: #{rbs_type}"
34
+ when :cast
35
+ srb_type = call.arguments&.arguments&.last #: as !nil
36
+ rbs_type = RBI::Type.parse_node(srb_type).rbs_string
37
+ "#: as #{rbs_type}"
38
+ when :must
39
+ "#: as !nil"
40
+ when :unsafe
41
+ "#: as untyped"
42
+ else
43
+ raise "Unknown annotation method: #{call.name}"
44
+ end
45
+ end
46
+
47
+ # Is this node a `T` or `::T` constant?
48
+ #: (Prism::Node?) -> bool
49
+ def t?(node)
50
+ case node
51
+ when Prism::ConstantReadNode
52
+ node.name == :T
53
+ when Prism::ConstantPathNode
54
+ node.parent.nil? && node.name == :T
55
+ else
56
+ false
57
+ end
58
+ end
59
+
60
+ # Is this node a `T.let` or `T.cast`?
61
+ #: (Prism::CallNode) -> bool
62
+ def t_annotation?(node)
63
+ return false unless t?(node.receiver)
64
+
65
+ case node.name
66
+ when :let, :cast
67
+ return node.arguments&.arguments&.size == 2
68
+ when :must, :unsafe
69
+ return node.arguments&.arguments&.size == 1
70
+ end
71
+
72
+ false
73
+ end
74
+
75
+ #: (Prism::Node) -> bool
76
+ def at_end_of_line?(node)
77
+ end_offset = node.location.end_offset
78
+ end_offset += 1 while (@ruby_bytes[end_offset] == " ".ord) && (end_offset < @ruby_bytes.size)
79
+ @ruby_bytes[end_offset] == LINE_BREAK
80
+ end
81
+
82
+ #: (Prism::Node, Prism::Node) -> String
83
+ def dedent_value(assign, value)
84
+ if value.location.start_line == assign.location.start_line
85
+ # The value is on the same line as the assign, so we can just return the slice as is:
86
+ # ```rb
87
+ # a = T.let(nil, T.nilable(String))
88
+ # ```
89
+ # becomes
90
+ # ```rb
91
+ # a = nil #: String?
92
+ # ```
93
+ return value.slice
94
+ end
95
+
96
+ # The value is on a different line, so we need to dedent it:
97
+ # ```rb
98
+ # a = T.let(
99
+ # [
100
+ # 1, 2, 3,
101
+ # ],
102
+ # T::Array[Integer],
103
+ # )
104
+ # ```
105
+ # becomes
106
+ # ```rb
107
+ # a = [
108
+ # 1, 2, 3,
109
+ # ] #: Array[Integer]
110
+ # ```
111
+ indent = value.location.start_line - assign.location.start_line
112
+ lines = value.slice.lines
113
+ if lines.size > 1
114
+ lines[1..]&.each_with_index do |line, i|
115
+ lines[i + 1] = line.delete_prefix(" " * indent)
116
+ end
117
+ end
118
+ lines.join
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,293 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Translate
7
+ # Converts all `sig` nodes to RBS comments in the given Ruby code.
8
+ # It also handles type members and class annotations.
9
+ class SorbetSigsToRBSComments < Translator
10
+ #: (String, file: String, positional_names: bool) -> void
11
+ def initialize(ruby_contents, file:, positional_names:)
12
+ super(ruby_contents, file: file)
13
+
14
+ @positional_names = positional_names #: bool
15
+ @nesting = [] #: Array[Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode]
16
+ @last_sigs = [] #: Array[[Prism::CallNode, RBI::Sig]]
17
+ @type_members = [] #: Array[String]
18
+ end
19
+
20
+ # @override
21
+ #: (Prism::ClassNode) -> void
22
+ def visit_class_node(node)
23
+ visit_scope(node) { super }
24
+ end
25
+
26
+ # @override
27
+ #: (Prism::ModuleNode) -> void
28
+ def visit_module_node(node)
29
+ visit_scope(node) { super }
30
+ end
31
+
32
+ # @override
33
+ #: (Prism::SingletonClassNode) -> void
34
+ def visit_singleton_class_node(node)
35
+ visit_scope(node) { super }
36
+ end
37
+
38
+ # @override
39
+ #: (Prism::DefNode) -> void
40
+ def visit_def_node(node)
41
+ return if @last_sigs.empty?
42
+ return if @last_sigs.any? { |_, sig| sig.is_abstract }
43
+
44
+ apply_member_annotations(@last_sigs)
45
+
46
+ # Build the RBI::Method node so we can print the method signature as RBS.
47
+ builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
48
+ builder.visit(node)
49
+ rbi_node = builder.tree.nodes.first #: as RBI::Method
50
+
51
+ @last_sigs.each do |node, sig|
52
+ out = StringIO.new
53
+ p = RBI::RBSPrinter.new(out: out, indent: node.location.start_column, positional_names: @positional_names)
54
+ p.print("#: ")
55
+ p.send(:print_method_sig, rbi_node, sig)
56
+ p.print("\n")
57
+ @rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out.string)
58
+ end
59
+
60
+ @last_sigs.clear
61
+ end
62
+
63
+ # @override
64
+ #: (Prism::CallNode) -> void
65
+ def visit_call_node(node)
66
+ case node.message
67
+ when "sig"
68
+ visit_sig(node)
69
+ when "attr_reader", "attr_writer", "attr_accessor"
70
+ visit_attr(node)
71
+ when "extend"
72
+ visit_extend(node)
73
+ when "abstract!", "interface!", "sealed!", "final!", "requires_ancestor"
74
+ visit_class_annotation(node)
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ # @override
81
+ #: (Prism::ConstantWriteNode) -> void
82
+ def visit_constant_write_node(node)
83
+ call = node.value
84
+ return super unless call.is_a?(Prism::CallNode)
85
+ return super unless call.message == "type_member"
86
+
87
+ @type_members << build_type_member_string(node)
88
+
89
+ from = adjust_to_line_start(node.location.start_offset)
90
+ to = adjust_to_line_end(node.location.end_offset)
91
+ to = adjust_to_new_line(to)
92
+
93
+ @rewriter << Source::Delete.new(from, to)
94
+ end
95
+
96
+ private
97
+
98
+ #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) { -> void } -> void
99
+ def visit_scope(node, &block)
100
+ @nesting << node
101
+ old_type_members = @type_members
102
+ @type_members = []
103
+
104
+ yield
105
+
106
+ if @type_members.any?
107
+ indent = " " * node.location.start_column
108
+ @rewriter << Source::Insert.new(node.location.start_offset, "#: [#{@type_members.join(", ")}]\n#{indent}")
109
+ end
110
+
111
+ @type_members = old_type_members
112
+ @nesting.pop
113
+ end
114
+
115
+ #: (Prism::CallNode) -> void
116
+ def visit_sig(node)
117
+ return unless sorbet_sig?(node)
118
+
119
+ builder = RBI::Parser::SigBuilder.new(@ruby_contents, file: @file)
120
+ builder.current.loc = node.location
121
+ builder.visit_call_node(node)
122
+ builder.current.comments = []
123
+
124
+ @last_sigs << [node, builder.current]
125
+ end
126
+
127
+ #: (Prism::CallNode) -> void
128
+ def visit_attr(node)
129
+ unless node.message == "attr_reader" || node.message == "attr_writer" || node.message == "attr_accessor"
130
+ raise Error, "Expected attr_reader, attr_writer, or attr_accessor"
131
+ end
132
+
133
+ return if @last_sigs.empty?
134
+ return if @last_sigs.any? { |_, sig| sig.is_abstract }
135
+
136
+ apply_member_annotations(@last_sigs)
137
+
138
+ builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
139
+ builder.visit(node)
140
+ rbi_node = builder.tree.nodes.first #: as RBI::Attr
141
+
142
+ @last_sigs.each do |node, sig|
143
+ out = StringIO.new
144
+ p = RBI::RBSPrinter.new(out: out, indent: node.location.start_column, positional_names: @positional_names)
145
+ p.print("#: ")
146
+ p.print_attr_sig(rbi_node, sig)
147
+ p.print("\n")
148
+ @rewriter << Source::Replace.new(node.location.start_offset, node.location.end_offset, out.string)
149
+ end
150
+
151
+ @last_sigs.clear
152
+ end
153
+
154
+ #: (Prism::CallNode node) -> void
155
+ def visit_extend(node)
156
+ raise Error, "Expected extend" unless node.message == "extend"
157
+
158
+ return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
159
+ return unless node.arguments&.arguments&.size == 1
160
+
161
+ arg = node.arguments&.arguments&.first
162
+ return unless arg.is_a?(Prism::ConstantPathNode)
163
+ return unless arg.slice.match?(/^(::)?T::Helpers$/) || arg.slice.match?(/^(::)?T::Generic$/)
164
+
165
+ from = adjust_to_line_start(node.location.start_offset)
166
+ to = adjust_to_line_end(node.location.end_offset)
167
+ to = adjust_to_new_line(to)
168
+ @rewriter << Source::Delete.new(from, to)
169
+ end
170
+
171
+ #: (Prism::CallNode node) -> void
172
+ def visit_class_annotation(node)
173
+ unless node.message == "abstract!" || node.message == "interface!" || node.message == "sealed!" ||
174
+ node.message == "final!" || node.message == "requires_ancestor"
175
+ raise Error, "Expected abstract!, interface!, sealed!, final!, or requires_ancestor"
176
+ end
177
+
178
+ return unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
179
+ return unless node.arguments.nil?
180
+
181
+ klass = @nesting.last #: as Prism::Node
182
+ indent = " " * klass.location.start_column
183
+
184
+ case node.message
185
+ when "abstract!"
186
+ @rewriter << Source::Insert.new(klass.location.start_offset, "# @abstract\n#{indent}")
187
+ when "interface!"
188
+ @rewriter << Source::Insert.new(klass.location.start_offset, "# @interface\n#{indent}")
189
+ when "sealed!"
190
+ @rewriter << Source::Insert.new(klass.location.start_offset, "# @sealed\n#{indent}")
191
+ when "final!"
192
+ @rewriter << Source::Insert.new(klass.location.start_offset, "# @final\n#{indent}")
193
+ when "requires_ancestor"
194
+ block = node.block
195
+ return unless block.is_a?(Prism::BlockNode)
196
+
197
+ body = block.body
198
+ return unless body.is_a?(Prism::StatementsNode)
199
+ return unless body.body.size == 1
200
+
201
+ arg = body.body.first #: as Prism::Node
202
+ srb_type = RBI::Type.parse_node(arg)
203
+ @rewriter << Source::Insert.new(klass.location.start_offset, "# @requires_ancestor: #{srb_type.rbs_string}\n#{indent}")
204
+ end
205
+
206
+ from = adjust_to_line_start(node.location.start_offset)
207
+ to = adjust_to_line_end(node.location.end_offset)
208
+ to = adjust_to_new_line(to)
209
+
210
+ @rewriter << Source::Delete.new(from, to)
211
+ end
212
+
213
+ #: (Array[[Prism::CallNode, RBI::Sig]]) -> void
214
+ def apply_member_annotations(sigs)
215
+ return if sigs.empty?
216
+
217
+ node, _sig = sigs.first #: as [Prism::CallNode, RBI::Sig]
218
+ insert_pos = node.location.start_offset
219
+
220
+ if sigs.any? { |_, sig| sig.without_runtime }
221
+ @rewriter << Source::Insert.new(insert_pos, "# @without_runtime\n")
222
+ end
223
+
224
+ if sigs.any? { |_, sig| sig.is_final }
225
+ @rewriter << Source::Insert.new(insert_pos, "# @final\n")
226
+ end
227
+
228
+ if sigs.any? { |_, sig| sig.is_abstract }
229
+ @rewriter << Source::Insert.new(insert_pos, "# @abstract\n")
230
+ end
231
+
232
+ if sigs.any? { |_, sig| sig.is_override }
233
+ @rewriter << if sigs.any? { |_, sig| sig.allow_incompatible_override }
234
+ Source::Insert.new(insert_pos, "# @override(allow_incompatible: true)\n")
235
+ else
236
+ Source::Insert.new(insert_pos, "# @override\n")
237
+ end
238
+ end
239
+
240
+ if sigs.any? { |_, sig| sig.is_overridable }
241
+ @rewriter << Source::Insert.new(insert_pos, "# @overridable\n")
242
+ end
243
+ end
244
+
245
+ #: (Prism::ConstantWriteNode) -> String
246
+ def build_type_member_string(node)
247
+ call = node.value
248
+ raise Error, "Expected a call node" unless call.is_a?(Prism::CallNode)
249
+ raise Error, "Expected type_member" unless call.message == "type_member"
250
+
251
+ type_member = node.name.to_s
252
+
253
+ arg = call.arguments&.arguments&.first
254
+ if arg.is_a?(Prism::SymbolNode)
255
+ case arg.slice
256
+ when ":in"
257
+ type_member = "in #{type_member}"
258
+ when ":out"
259
+ type_member = "out #{type_member}"
260
+ else
261
+ raise Error, "Unknown type member variance: #{arg.slice}"
262
+ end
263
+ end
264
+
265
+ block = call.block
266
+ return type_member unless block.is_a?(Prism::BlockNode)
267
+
268
+ body = block.body
269
+ return type_member unless body.is_a?(Prism::StatementsNode)
270
+ return type_member unless body.body.size == 1
271
+
272
+ hash = body.body.first
273
+ return type_member unless hash.is_a?(Prism::HashNode)
274
+
275
+ hash.elements.each do |element|
276
+ next unless element.is_a?(Prism::AssocNode)
277
+
278
+ type = RBI::Type.parse_node(element.value)
279
+
280
+ case element.key.slice
281
+ when "upper:"
282
+ type_member = "#{type_member} < #{type.rbs_string}"
283
+ when "fixed:"
284
+ type_member = "#{type_member} = #{type.rbs_string}"
285
+ end
286
+ end
287
+
288
+ type_member
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end