spoom 1.7.15 → 1.8.0

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.
@@ -0,0 +1,78 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Spoom
5
+ module Sorbet
6
+ module Translate
7
+ module RBSCommentsToSorbetSigs
8
+ # @abstract
9
+ class BaseRBIFormat
10
+ end
11
+
12
+ class HumanReadableRBIFormat < BaseRBIFormat
13
+ #: Integer?
14
+ attr_reader :max_line_length
15
+
16
+ #: (
17
+ #| ?max_line_length: Integer?,
18
+ #| ) -> void
19
+ def initialize(max_line_length: nil)
20
+ super()
21
+ @max_line_length = max_line_length
22
+
23
+ freeze
24
+ end
25
+
26
+ @default = new #: HumanReadableRBIFormat
27
+ class << self
28
+ #: HumanReadableRBIFormat
29
+ attr_reader :default
30
+ end
31
+ end
32
+
33
+ class LineMatchedRBIFormat < BaseRBIFormat
34
+ @default = new #: LineMatchedRBIFormat
35
+ class << self
36
+ #: LineMatchedRBIFormat
37
+ attr_reader :default
38
+ end
39
+ end
40
+
41
+ class Options
42
+ #: Symbol
43
+ attr_reader :overloads_strategy
44
+
45
+ ALLOWED_OVERLOAD_STRATEGIES = [:translate_all, :translate_last, :raise].freeze
46
+
47
+ #: BaseRBIFormat
48
+ attr_reader :output_format
49
+
50
+ #: (
51
+ #| ?overloads_strategy: Symbol,
52
+ #| ?output_format: BaseRBIFormat,
53
+ #| ) -> void
54
+ def initialize(
55
+ overloads_strategy: :translate_all,
56
+ output_format: HumanReadableRBIFormat.default
57
+ )
58
+ unless ALLOWED_OVERLOAD_STRATEGIES.include?(overloads_strategy)
59
+ raise ArgumentError, "Unknown overloads_strategy: #{overloads_strategy.inspect}. " \
60
+ "Must be one of: #{ALLOWED_OVERLOAD_STRATEGIES.map(&:inspect).join(", ")}"
61
+ end
62
+
63
+ @overloads_strategy = overloads_strategy
64
+ @output_format = output_format
65
+
66
+ freeze
67
+ end
68
+
69
+ @default = new #: Options
70
+ class << self
71
+ #: Options
72
+ attr_reader :default
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -4,395 +4,54 @@
4
4
  module Spoom
5
5
  module Sorbet
6
6
  module Translate
7
- class RBSCommentsToSorbetSigs < Translator
8
- include Spoom::RBS::ExtractRBSComments
9
-
10
- RBS_ANNOTATION_MARKERS = [
11
- "# @abstract",
12
- "# @interface",
13
- "# @sealed",
14
- "# @final",
15
- "# @requires_ancestor:",
16
- "# @override",
17
- "# @overridable",
18
- "# @without_runtime",
19
- ].freeze #: Array[String]
20
- RBS_REWRITE_PATTERN = Regexp.union(["#:", "#|", *RBS_ANNOTATION_MARKERS]).freeze #: Regexp
21
- private_constant :RBS_ANNOTATION_MARKERS, :RBS_REWRITE_PATTERN
22
-
7
+ module RBSCommentsToSorbetSigs
23
8
  class << self
9
+ RBS_ANNOTATION_MARKERS = [
10
+ "# @abstract",
11
+ "# @interface",
12
+ "# @sealed",
13
+ "# @final",
14
+ "# @requires_ancestor:",
15
+ "# @override",
16
+ "# @overridable",
17
+ "# @without_runtime",
18
+ ].freeze #: Array[String]
19
+ RBS_REWRITE_PATTERN = Regexp.union(["#:", "#|", *RBS_ANNOTATION_MARKERS]).freeze #: Regexp
20
+ private_constant :RBS_ANNOTATION_MARKERS, :RBS_REWRITE_PATTERN
21
+
24
22
  #: (String source) -> bool
25
23
  def contains_rbs_syntax?(source)
26
24
  Sigils.contains_valid_sigil?(source) && source.match?(RBS_REWRITE_PATTERN)
27
25
  end
28
26
 
29
- #: (String ruby_contents, file: String, ?max_line_length: Integer?) -> String
30
- def rewrite_if_needed(ruby_contents, file:, max_line_length: nil)
27
+ #: (
28
+ #| String ruby_contents,
29
+ #| file: String,
30
+ #| ?max_line_length: Integer?,
31
+ #| ?overloads_strategy: Symbol) -> String
32
+ def rewrite_if_needed(
33
+ ruby_contents,
34
+ file:,
35
+ max_line_length: nil,
36
+ overloads_strategy: :translate_all
37
+ )
31
38
  return ruby_contents unless contains_rbs_syntax?(ruby_contents)
32
39
 
33
- new(ruby_contents, file:, max_line_length:).rewrite
34
- end
35
- end
36
-
37
- #: (String, file: String, ?max_line_length: Integer?) -> void
38
- def initialize(ruby_contents, file:, max_line_length: nil)
39
- super(ruby_contents, file: file)
40
-
41
- @max_line_length = max_line_length
42
- end
43
-
44
- # @override
45
- #: (Prism::ProgramNode node) -> void
46
- def visit_program_node(node)
47
- # Process all type aliases from the entire file first
48
- apply_type_aliases(@comments)
49
-
50
- # Now process the rest of the file with type aliases available
51
- super
52
- end
53
-
54
- # @override
55
- #: (Prism::ClassNode node) -> void
56
- def visit_class_node(node)
57
- apply_class_annotations(node)
58
-
59
- super
60
- end
61
-
62
- # @override
63
- #: (Prism::ModuleNode node) -> void
64
- def visit_module_node(node)
65
- apply_class_annotations(node)
66
-
67
- super
68
- end
69
-
70
- # @override
71
- #: (Prism::SingletonClassNode node) -> void
72
- def visit_singleton_class_node(node)
73
- apply_class_annotations(node)
74
-
75
- super
76
- end
77
-
78
- # @override
79
- #: (Prism::DefNode node) -> void
80
- def visit_def_node(node)
81
- rewrite_def(node, node_rbs_comments(node))
82
- end
83
-
84
- # @override
85
- #: (Prism::CallNode node) -> void
86
- def visit_call_node(node)
87
- case node.message
88
- when "attr_reader", "attr_writer", "attr_accessor"
89
- visit_attr(node)
90
- else
91
- def_node = node.arguments&.arguments&.first
92
- if def_node&.is_a?(Prism::DefNode)
93
- rewrite_def(def_node, node_rbs_comments(node))
94
- return
95
- end
96
-
97
- super
98
- end
99
- end
100
-
101
- private
102
-
103
- #: (Prism::CallNode) -> void
104
- def visit_attr(node)
105
- comments = node_rbs_comments(node)
106
- return if comments.empty?
107
-
108
- return if comments.signatures.empty?
109
-
110
- comments.signatures.each do |signature|
111
- attr_type = ::RBS::Parser.parse_type(signature.string)
112
- sig = RBI::Sig.new
113
-
114
- if node.message == "attr_writer"
115
- if node.arguments&.arguments&.size != 1
116
- raise Error, "AttrWriter must have exactly one name"
117
- end
118
-
119
- name = node.arguments&.arguments&.first #: as Prism::SymbolNode
120
- sig.params << RBI::SigParam.new(
121
- name.slice[1..-1], #: as String
122
- RBI::RBS::TypeTranslator.translate(attr_type),
123
- )
124
- end
125
-
126
- sig.return_type = RBI::RBS::TypeTranslator.translate(attr_type)
127
-
128
- apply_member_annotations(comments.method_annotations, sig)
129
-
130
- @rewriter << Source::Replace.new(
131
- signature.location.start_offset,
132
- signature.location.end_offset,
133
- sig.string(max_line_length: @max_line_length),
40
+ options = Options.new(
41
+ overloads_strategy:,
42
+ output_format: HumanReadableRBIFormat.new(
43
+ max_line_length:,
44
+ ),
134
45
  )
135
- rescue ::RBS::ParsingError, ::RBI::Error
136
- # Ignore signatures with errors
137
- next
138
- end
139
- end
140
46
 
141
- #: (Prism::DefNode, RBS::Comments) -> void
142
- def rewrite_def(def_node, comments)
143
- return if comments.empty?
144
- return if comments.signatures.empty?
145
-
146
- builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
147
- builder.visit(def_node)
148
- rbi_node = builder.tree.nodes.first #: as RBI::Method
149
-
150
- comments.signatures.each do |signature|
151
- begin
152
- method_type = ::RBS::Parser.parse_method_type(signature.string)
153
- rescue ::RBS::ParsingError
154
- next
155
- end
156
-
157
- translator = RBI::RBS::MethodTypeTranslator.new(rbi_node)
158
-
159
- begin
160
- translator.visit(method_type)
161
- rescue ::RBI::Error
162
- next
163
- end
164
-
165
- sig = translator.result
166
-
167
- apply_member_annotations(comments.method_annotations, sig)
168
-
169
- # Sorbet runtime doesn't support `sig` on `method_added` or
170
- # `singleton_method_added`, so we always use `without_runtime` for them.
171
- if def_node.name == :method_added || def_node.name == :singleton_method_added
172
- sig.without_runtime = true
173
- end
174
-
175
- @rewriter << Source::Replace.new(
176
- signature.location.start_offset,
177
- signature.location.end_offset,
178
- sig.string(max_line_length: @max_line_length),
179
- )
180
- end
181
- end
182
-
183
- #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) -> void
184
- def apply_class_annotations(node)
185
- comments = node_rbs_comments(node)
186
- return if comments.empty?
187
-
188
- indent = " " * (node.location.start_column + 2)
189
- insert_pos = case node
190
- when Prism::ClassNode
191
- (node.superclass || node.constant_path).location.end_offset
192
- when Prism::ModuleNode
193
- node.constant_path.location.end_offset
194
- when Prism::SingletonClassNode
195
- node.expression.location.end_offset
196
- end
197
-
198
- class_annotations = comments.class_annotations
199
- if class_annotations.any?
200
- unless already_extends?(node, /^(::)?T::Helpers$/)
201
- @rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Helpers\n")
202
- end
203
-
204
- class_annotations.reverse_each do |annotation|
205
- from = adjust_to_line_start(annotation.location.start_offset)
206
- to = adjust_to_line_end(annotation.location.end_offset)
207
-
208
- content = case annotation.string
209
- when "@abstract"
210
- "abstract!"
211
- when "@interface"
212
- "interface!"
213
- when "@sealed"
214
- "sealed!"
215
- when "@final"
216
- "final!"
217
- when /^@requires_ancestor: /
218
- srb_type = ::RBS::Parser.parse_type(annotation.string.delete_prefix("@requires_ancestor: "))
219
- rbs_type = RBI::RBS::TypeTranslator.translate(srb_type)
220
- "requires_ancestor { #{rbs_type} }"
221
- else
222
- next
223
- end
224
-
225
- @rewriter << Source::Delete.new(from, to)
226
-
227
- newline = node.body.nil? ? "" : "\n"
228
- @rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{content}#{newline}")
229
- rescue ::RBS::ParsingError, ::RBI::Error
230
- # Ignore annotations with errors
231
- next
232
- end
233
- end
234
-
235
- signatures = comments.signatures
236
- if signatures.any?
237
- signatures.each do |signature|
238
- # Only type param signatures (e.g. `#: [A, B]`) are valid on class/module nodes
239
- next unless signature.string.start_with?("[")
240
-
241
- type_params = ::RBS::Parser.parse_type_params(signature.string)
242
- next if type_params.empty?
243
-
244
- from = adjust_to_line_start(signature.location.start_offset)
245
- to = adjust_to_line_end(signature.location.end_offset)
246
- @rewriter << Source::Delete.new(from, to)
247
-
248
- unless already_extends?(node, /^(::)?T::Generic$/)
249
- @rewriter << Source::Insert.new(insert_pos, "\n#{indent}extend T::Generic\n")
250
- end
251
-
252
- type_params.each do |type_param|
253
- type_member = "#{type_param.name} = type_member"
254
-
255
- case type_param.variance
256
- when :covariant
257
- type_member = "#{type_member}(:out)"
258
- when :contravariant
259
- type_member = "#{type_member}(:in)"
260
- end
261
-
262
- if type_param.upper_bound || type_param.default_type
263
- if type_param.upper_bound
264
- rbs_type = RBI::RBS::TypeTranslator.translate(type_param.upper_bound)
265
- type_member = "#{type_member} {{ upper: #{rbs_type} }}"
266
- end
267
-
268
- if type_param.default_type
269
- rbs_type = RBI::RBS::TypeTranslator.translate(type_param.default_type)
270
- type_member = "#{type_member} {{ fixed: #{rbs_type} }}"
271
- end
272
- end
273
-
274
- newline = node.body.nil? ? "" : "\n"
275
- @rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
276
- rescue ::RBS::ParsingError, ::RBI::Error
277
- # Ignore signatures with errors
278
- next
279
- end
280
- end
281
- end
282
- end
283
-
284
- #: (Array[RBS::Annotation], RBI::Sig) -> void
285
- def apply_member_annotations(annotations, sig)
286
- annotations.each do |annotation|
287
- case annotation.string
288
- when "@abstract"
289
- sig.is_abstract = true
290
- when "@final"
291
- sig.is_final = true
292
- when "@override"
293
- sig.is_override = true
294
- when "@override(allow_incompatible: true)"
295
- sig.is_override = true
296
- sig.allow_incompatible_override = true
297
- when "@override(allow_incompatible: :visibility)"
298
- sig.is_override = true
299
- sig.allow_incompatible_override_visibility = true
300
- when "@overridable"
301
- sig.is_overridable = true
302
- when "@without_runtime"
303
- sig.without_runtime = true
304
- end
305
- end
306
- end
307
-
308
- #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode, Regexp) -> bool
309
- def already_extends?(node, constant_regex)
310
- node.child_nodes.any? do |c|
311
- next false unless c.is_a?(Prism::CallNode)
312
- next false unless c.message == "extend"
313
- next false unless c.receiver.nil? || c.receiver.is_a?(Prism::SelfNode)
314
- next false unless c.arguments&.arguments&.size == 1
315
-
316
- arg = c.arguments&.arguments&.first
317
- next false unless arg.is_a?(Prism::ConstantPathNode)
318
- next false unless arg.slice.match?(constant_regex)
319
-
320
- true
321
- end
322
- end
323
-
324
- #: (Array[Prism::Comment]) -> Array[RBS::TypeAlias]
325
- def collect_type_aliases(comments)
326
- type_aliases = [] #: Array[RBS::TypeAlias]
327
-
328
- return type_aliases if comments.empty?
329
-
330
- continuation_comments = [] #: Array[Prism::Comment]
331
-
332
- comments.reverse_each do |comment|
333
- string = comment.slice
334
-
335
- if string.start_with?("#:")
336
- string = string.delete_prefix("#:").strip
337
- location = comment.location
338
-
339
- if string.start_with?("type ")
340
- continuation_comments.reverse_each do |continuation_comment|
341
- string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
342
- location = location.join(continuation_comment.location)
343
- end
344
-
345
- type_aliases << Spoom::RBS::TypeAlias.new(string, location)
346
- end
347
-
348
- # Clear the continuation comments regardless of whether we found a type alias or not
349
- continuation_comments.clear
350
- elsif string.start_with?("#|")
351
- continuation_comments << comment
352
- else
353
- continuation_comments.clear
354
- end
355
- end
356
-
357
- type_aliases
358
- end
359
-
360
- #: (Array[Prism::Comment]) -> void
361
- def apply_type_aliases(comments)
362
- type_aliases = collect_type_aliases(comments)
363
-
364
- type_aliases.each do |type_alias|
365
- indent = " " * type_alias.location.start_column
366
- insert_pos = adjust_to_line_start(type_alias.location.start_offset)
367
-
368
- from = insert_pos
369
- to = adjust_to_line_end(type_alias.location.end_offset)
370
-
371
- *, decls = ::RBS::Parser.parse_signature(type_alias.string)
372
-
373
- # We only expect there to be a single type alias declaration
374
- next unless decls.size == 1 && decls.first.is_a?(::RBS::AST::Declarations::TypeAlias)
375
-
376
- rbs_type = decls.first
377
- sorbet_type = RBI::RBS::TypeTranslator.translate(rbs_type.type)
378
-
379
- alias_name = ::RBS::TypeName.new(
380
- namespace: rbs_type.name.namespace,
381
- name: rbs_type.name.name.to_s.gsub(/(?:^|_)([a-z\d]*)/i) do |match|
382
- match = match.delete_prefix("_")
383
- !match.empty? ? match[0].upcase.concat(match[1..-1]) : +""
384
- end,
385
- )
386
-
387
- @rewriter << Source::Delete.new(from, to)
388
- content = "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n"
389
- @rewriter << Source::Insert.new(insert_pos, content)
390
- rescue ::RBS::ParsingError, ::RBI::Error
391
- # Ignore type aliases with errors
392
- next
47
+ HumanReadableTranslator.new(ruby_contents, file:, options:).rewrite
393
48
  end
394
49
  end
395
50
  end
396
51
  end
397
52
  end
398
53
  end
54
+
55
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options"
56
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/human_readable_translator"
57
+ require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/line_matching_translator"
@@ -134,7 +134,7 @@ module Spoom
134
134
 
135
135
  private
136
136
 
137
- #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode) { -> void } -> void
137
+ #: (PrismTypes::anyScopeNode) { -> void } -> void
138
138
  def visit_scope(node, &block)
139
139
  old_class_annotations = @class_annotations
140
140
  @class_annotations = []
@@ -229,7 +229,7 @@ module Spoom
229
229
  end
230
230
  end
231
231
 
232
- #: (Prism::ClassNode | Prism::ModuleNode | Prism::SingletonClassNode, Prism::CallNode) -> void
232
+ #: (PrismTypes::anyScopeNode, Prism::CallNode) -> void
233
233
  def apply_class_annotation(parent, node)
234
234
  unless node.message == "abstract!" || node.message == "interface!" || node.message == "sealed!" ||
235
235
  node.message == "final!" || node.message == "requires_ancestor"