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