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.
- checksums.yaml +4 -4
- data/lib/spoom/deadcode/plugins/actionpack.rb +4 -2
- data/lib/spoom/deadcode/plugins/active_model.rb +44 -2
- data/lib/spoom/deadcode/plugins/active_record.rb +10 -7
- data/lib/spoom/deadcode/plugins/base.rb +4 -3
- data/lib/spoom/deadcode/plugins/graphql.rb +42 -5
- data/lib/spoom/deadcode/plugins/minitest.rb +3 -3
- data/lib/spoom/deadcode/plugins/ruby.rb +1 -1
- data/lib/spoom/ext/prism_types.rb +14 -0
- data/lib/spoom/model/builder.rb +3 -3
- data/lib/spoom/rbs.rb +20 -2
- data/lib/spoom/sorbet/metrics/code_metrics_visitor.rb +3 -3
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator.rb +484 -0
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/human_readable_translator.rb +72 -0
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/line_matching_translator.rb +115 -0
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options.rb +78 -0
- data/lib/spoom/sorbet/translate/rbs_comments_to_sorbet_sigs.rb +35 -419
- data/lib/spoom/sorbet/translate/sorbet_sigs_to_rbs_comments.rb +2 -2
- data/lib/spoom/sorbet/translate/validator.rb +214 -0
- data/lib/spoom/sorbet/translate.rb +1 -0
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +2 -0
- data/rbi/spoom.rbi +306 -20
- metadata +8 -2
|
@@ -0,0 +1,484 @@
|
|
|
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 BaseTranslator < Translator
|
|
10
|
+
include Spoom::RBS::ExtractRBSComments
|
|
11
|
+
|
|
12
|
+
#: (String, file: String, ?options: Options) -> void
|
|
13
|
+
def initialize(
|
|
14
|
+
ruby_contents,
|
|
15
|
+
file:,
|
|
16
|
+
options: Options.default
|
|
17
|
+
)
|
|
18
|
+
super(ruby_contents, file:)
|
|
19
|
+
|
|
20
|
+
@max_line_length = case (format = options.output_format)
|
|
21
|
+
when HumanReadableRBIFormat
|
|
22
|
+
format.max_line_length
|
|
23
|
+
else
|
|
24
|
+
nil
|
|
25
|
+
end #: Integer?
|
|
26
|
+
|
|
27
|
+
@overloads_strategy = options.overloads_strategy #: Symbol
|
|
28
|
+
@type_translator = RBI::RBS::TypeTranslator.new #: RBI::RBS::TypeTranslator
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @override
|
|
32
|
+
#: (Prism::ProgramNode node) -> void
|
|
33
|
+
def visit_program_node(node)
|
|
34
|
+
# Process all type aliases from the entire file first
|
|
35
|
+
apply_type_aliases(@comments)
|
|
36
|
+
|
|
37
|
+
# Now process the rest of the file with type aliases available
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @override
|
|
42
|
+
#: (Prism::ClassNode node) -> void
|
|
43
|
+
def visit_class_node(node)
|
|
44
|
+
apply_class_annotations(node)
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @override
|
|
50
|
+
#: (Prism::ModuleNode node) -> void
|
|
51
|
+
def visit_module_node(node)
|
|
52
|
+
apply_class_annotations(node)
|
|
53
|
+
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @override
|
|
58
|
+
#: (Prism::SingletonClassNode node) -> void
|
|
59
|
+
def visit_singleton_class_node(node)
|
|
60
|
+
apply_class_annotations(node)
|
|
61
|
+
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @override
|
|
66
|
+
#: (Prism::DefNode node) -> void
|
|
67
|
+
def visit_def_node(node)
|
|
68
|
+
rewrite_def(node, node_rbs_comments(node))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @override
|
|
72
|
+
#: (Prism::CallNode node) -> void
|
|
73
|
+
def visit_call_node(node)
|
|
74
|
+
case node.message
|
|
75
|
+
when "attr_reader", "attr_writer", "attr_accessor"
|
|
76
|
+
visit_attr(node)
|
|
77
|
+
else
|
|
78
|
+
def_node = node.arguments&.arguments&.first
|
|
79
|
+
if def_node&.is_a?(Prism::DefNode)
|
|
80
|
+
rewrite_def(def_node, node_rbs_comments(node))
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
#: (Prism::CallNode) -> void
|
|
91
|
+
def visit_attr(node)
|
|
92
|
+
comments = node_rbs_comments(node)
|
|
93
|
+
return if comments.empty?
|
|
94
|
+
|
|
95
|
+
return if comments.signatures.empty?
|
|
96
|
+
|
|
97
|
+
signatures = apply_overloads_strategy(
|
|
98
|
+
comments.signatures,
|
|
99
|
+
method_name: node.message.to_s,
|
|
100
|
+
location: "#{@file}:#{node.location.start_line}",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
known_annotations = nil #: Array[Spoom::RBS::Annotation]?
|
|
104
|
+
|
|
105
|
+
signatures.each do |signature|
|
|
106
|
+
attr_type = ::RBS::Parser.parse_type(signature.string)
|
|
107
|
+
sig = RBI::Sig.new
|
|
108
|
+
|
|
109
|
+
if node.message == "attr_writer"
|
|
110
|
+
if node.arguments&.arguments&.size != 1
|
|
111
|
+
raise Error, "AttrWriter must have exactly one name"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
name = node.arguments&.arguments&.first #: as Prism::SymbolNode
|
|
115
|
+
sig.params << RBI::SigParam.new(
|
|
116
|
+
name.slice[1..-1], #: as String
|
|
117
|
+
@type_translator.translate(attr_type),
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
sig.return_type = @type_translator.translate(attr_type)
|
|
122
|
+
|
|
123
|
+
known_annotations = apply_member_annotations(comments.method_annotations, sig)
|
|
124
|
+
|
|
125
|
+
@rewriter << Source::Replace.new(
|
|
126
|
+
signature.location.start_offset,
|
|
127
|
+
signature.location.end_offset,
|
|
128
|
+
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
|
|
129
|
+
)
|
|
130
|
+
rescue ::RBS::ParsingError, ::RBI::Error
|
|
131
|
+
# Ignore signatures with errors
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if known_annotations
|
|
136
|
+
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#: (Prism::DefNode, Spoom::RBS::Comments) -> void
|
|
141
|
+
def rewrite_def(def_node, comments)
|
|
142
|
+
return if comments.empty?
|
|
143
|
+
return if comments.signatures.empty?
|
|
144
|
+
|
|
145
|
+
signatures = apply_overloads_strategy(
|
|
146
|
+
comments.signatures,
|
|
147
|
+
method_name: def_node.name.to_s,
|
|
148
|
+
location: "#{@file}:#{def_node.location.start_line}",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
builder = RBI::Parser::TreeBuilder.new(@ruby_contents, comments: [], file: @file)
|
|
152
|
+
builder.visit(def_node)
|
|
153
|
+
rbi_node = builder.tree.nodes.first #: as RBI::Method
|
|
154
|
+
|
|
155
|
+
known_annotations = nil #: Array[Spoom::RBS::Annotation]?
|
|
156
|
+
|
|
157
|
+
signatures.each do |signature|
|
|
158
|
+
begin
|
|
159
|
+
method_type = ::RBS::Parser.parse_method_type(signature.string)
|
|
160
|
+
rescue ::RBS::ParsingError
|
|
161
|
+
next
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
translator = RBI::RBS::MethodTypeTranslator.new(rbi_node)
|
|
165
|
+
|
|
166
|
+
begin
|
|
167
|
+
translator.visit(method_type)
|
|
168
|
+
rescue ::RBI::Error
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
sig = translator.result
|
|
173
|
+
|
|
174
|
+
known_annotations = apply_member_annotations(comments.method_annotations, sig)
|
|
175
|
+
|
|
176
|
+
# Sorbet runtime doesn't support `sig` on `method_added` or
|
|
177
|
+
# `singleton_method_added`, so we always use `without_runtime` for them.
|
|
178
|
+
if def_node.name == :method_added || def_node.name == :singleton_method_added
|
|
179
|
+
sig.without_runtime = true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
@rewriter << Source::Replace.new(
|
|
183
|
+
signature.location.start_offset,
|
|
184
|
+
signature.location.end_offset,
|
|
185
|
+
pad_out_line_count(of: sig.string(max_line_length: @max_line_length), to_height_of: signature),
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if known_annotations
|
|
190
|
+
rewrite_member_annotations(comments.method_annotations, known: known_annotations)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
#: (Array[Spoom::RBS::Signature], method_name: String, location: String) -> Array[Spoom::RBS::Signature]
|
|
195
|
+
def apply_overloads_strategy(signatures, method_name:, location:)
|
|
196
|
+
return signatures if signatures.size <= 1
|
|
197
|
+
|
|
198
|
+
case @overloads_strategy
|
|
199
|
+
when :translate_all
|
|
200
|
+
signatures
|
|
201
|
+
when :translate_last
|
|
202
|
+
others = signatures[0...-1] #: as !nil
|
|
203
|
+
others.each { |signature| rewrite_discarded_overload(signature) }
|
|
204
|
+
|
|
205
|
+
kept = signatures.last #: as Spoom::RBS::Signature
|
|
206
|
+
[kept]
|
|
207
|
+
else # :raise
|
|
208
|
+
raise Error, "Method `#{method_name}` at #{location} has multiple overloaded signatures"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Called for every overloaded method sig that we discard because it wasn't the last one.
|
|
213
|
+
# @abstract
|
|
214
|
+
#: (Spoom::RBS::Signature) -> void
|
|
215
|
+
def rewrite_discarded_overload(signature) = raise
|
|
216
|
+
|
|
217
|
+
#: (PrismTypes::anyScopeNode) -> void
|
|
218
|
+
def apply_class_annotations(node)
|
|
219
|
+
comments = node_rbs_comments(node)
|
|
220
|
+
return if comments.empty?
|
|
221
|
+
|
|
222
|
+
insert_pos = case node
|
|
223
|
+
when Prism::ClassNode
|
|
224
|
+
(node.superclass || node.constant_path).location.end_offset
|
|
225
|
+
when Prism::ModuleNode
|
|
226
|
+
node.constant_path.location.end_offset
|
|
227
|
+
when Prism::SingletonClassNode
|
|
228
|
+
node.expression.location.end_offset
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Only translate (and `extend T::Helpers`) when there's at least one *known* class
|
|
232
|
+
# annotation. A node with only unknown annotations (e.g. `@private`) is left untouched.
|
|
233
|
+
if comments.class_annotations.any?
|
|
234
|
+
unless already_extends?(node, /^(::)?T::Helpers$/)
|
|
235
|
+
extend_with("T::Helpers", into: node, at: insert_pos)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
comments.annotations.reverse_each do |annotation|
|
|
239
|
+
content = case annotation.string
|
|
240
|
+
when "@abstract"
|
|
241
|
+
"abstract!"
|
|
242
|
+
when "@interface"
|
|
243
|
+
"interface!"
|
|
244
|
+
when "@sealed"
|
|
245
|
+
"sealed!"
|
|
246
|
+
when "@final"
|
|
247
|
+
"final!"
|
|
248
|
+
when /^@requires_ancestor: /
|
|
249
|
+
srb_type = ::RBS::Parser.parse_type(annotation.string.delete_prefix("@requires_ancestor: "))
|
|
250
|
+
rbs_type = @type_translator.translate(srb_type)
|
|
251
|
+
"requires_ancestor { #{rbs_type} }"
|
|
252
|
+
else
|
|
253
|
+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
|
|
254
|
+
next
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: content)
|
|
258
|
+
rescue ::RBS::ParsingError, ::RBI::Error
|
|
259
|
+
apply_class_annotation(annotation, parent_node: node, insert_pos:, sorbet_replacement: nil)
|
|
260
|
+
next
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
signatures = comments.signatures
|
|
265
|
+
if signatures.any?
|
|
266
|
+
signatures.each do |signature|
|
|
267
|
+
# Only type param signatures (e.g. `#: [A, B]`) are valid on class/module nodes
|
|
268
|
+
next unless signature.string.start_with?("[")
|
|
269
|
+
|
|
270
|
+
type_params = ::RBS::Parser.parse_type_params(signature.string)
|
|
271
|
+
rewrite_type_params_signature(signature, type_params:)
|
|
272
|
+
next if type_params.empty?
|
|
273
|
+
|
|
274
|
+
unless already_extends?(node, /^(::)?T::Generic$/)
|
|
275
|
+
extend_with("T::Generic", into: node, at: insert_pos)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
type_params.each do |type_param|
|
|
279
|
+
type_member = "#{type_param.name} = type_member"
|
|
280
|
+
|
|
281
|
+
case type_param.variance
|
|
282
|
+
when :covariant
|
|
283
|
+
type_member = "#{type_member}(:out)"
|
|
284
|
+
when :contravariant
|
|
285
|
+
type_member = "#{type_member}(:in)"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
if type_param.upper_bound || type_param.default_type
|
|
289
|
+
if type_param.upper_bound
|
|
290
|
+
rbs_type = @type_translator.translate(type_param.upper_bound)
|
|
291
|
+
type_member = "#{type_member} {{ upper: #{rbs_type} }}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if type_param.default_type
|
|
295
|
+
rbs_type = @type_translator.translate(type_param.default_type)
|
|
296
|
+
type_member = "#{type_member} {{ fixed: #{rbs_type} }}"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
insert_type_member(type_member, parent_node: node, insert_pos:)
|
|
301
|
+
rescue ::RBS::ParsingError, ::RBI::Error
|
|
302
|
+
# Ignore signatures with errors
|
|
303
|
+
next
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# @param is_known: true if this is an RBS annotation that we recognize
|
|
310
|
+
# false for some other `@`-prefixed thing, like a documentation `@param` tag.
|
|
311
|
+
# @abstract
|
|
312
|
+
#: (
|
|
313
|
+
#| Spoom::RBS::Annotation,
|
|
314
|
+
#| parent_node: PrismTypes::anyScopeNode,
|
|
315
|
+
#| insert_pos: Integer,
|
|
316
|
+
#| sorbet_replacement: String?
|
|
317
|
+
#| ) -> void
|
|
318
|
+
def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:) = raise
|
|
319
|
+
|
|
320
|
+
# Rewrites the `#: [...]` type params comment (e.g. delete it, or mark it as translated).
|
|
321
|
+
# @abstract
|
|
322
|
+
#: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
|
|
323
|
+
def rewrite_type_params_signature(signature, type_params:) = raise
|
|
324
|
+
|
|
325
|
+
# Inserts a single `type_member` declaration into the class/module body.
|
|
326
|
+
# @abstract
|
|
327
|
+
#: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
|
|
328
|
+
def insert_type_member(type_member, parent_node:, insert_pos:) = raise
|
|
329
|
+
|
|
330
|
+
#: (Array[Spoom::RBS::Annotation], RBI::Sig) -> Array[Spoom::RBS::Annotation]
|
|
331
|
+
def apply_member_annotations(annotations, sig)
|
|
332
|
+
known = [] #: Array[Spoom::RBS::Annotation]
|
|
333
|
+
|
|
334
|
+
annotations.each do |annotation|
|
|
335
|
+
case annotation.string
|
|
336
|
+
when "@abstract"
|
|
337
|
+
sig.is_abstract = true
|
|
338
|
+
when "@final"
|
|
339
|
+
sig.is_final = true
|
|
340
|
+
when "@override"
|
|
341
|
+
sig.is_override = true
|
|
342
|
+
when "@override(allow_incompatible: true)"
|
|
343
|
+
sig.is_override = true
|
|
344
|
+
sig.allow_incompatible_override = true
|
|
345
|
+
when "@override(allow_incompatible: :visibility)"
|
|
346
|
+
sig.is_override = true
|
|
347
|
+
sig.allow_incompatible_override_visibility = true
|
|
348
|
+
when "@overridable"
|
|
349
|
+
sig.is_overridable = true
|
|
350
|
+
when "@without_runtime"
|
|
351
|
+
sig.without_runtime = true
|
|
352
|
+
else
|
|
353
|
+
next
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
known << annotation
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
known
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Rewrites the member annotation comments in the source. Called once per method,
|
|
363
|
+
# regardless of how many overloaded signatures share the annotations, to avoid
|
|
364
|
+
# emitting duplicate markers.
|
|
365
|
+
#
|
|
366
|
+
#: (Array[Spoom::RBS::Annotation], known: Array[Spoom::RBS::Annotation]) -> void
|
|
367
|
+
def rewrite_member_annotations(annotations, known:)
|
|
368
|
+
annotations.each do |annotation|
|
|
369
|
+
rewrite_annotation(annotation, is_known: known.include?(annotation))
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# @param is_known: true if this is an RBS annotation that we recognize
|
|
374
|
+
# false for some other `@`-prefixed thing, like a documentation `@param` tag.
|
|
375
|
+
# @overridable
|
|
376
|
+
#: (Spoom::RBS::Annotation, is_known: bool) -> void
|
|
377
|
+
def rewrite_annotation(annotation, is_known:) = nil # no-op
|
|
378
|
+
|
|
379
|
+
# @abstract
|
|
380
|
+
#: (String mixin_name, into: PrismTypes::anyScopeNode, at: Integer) -> void
|
|
381
|
+
def extend_with(mixin_name, into:, at:) = raise
|
|
382
|
+
|
|
383
|
+
#: (PrismTypes::anyScopeNode, Regexp) -> bool
|
|
384
|
+
def already_extends?(node, constant_regex)
|
|
385
|
+
node.child_nodes.any? do |c|
|
|
386
|
+
next false unless c.is_a?(Prism::CallNode)
|
|
387
|
+
next false unless c.message == "extend"
|
|
388
|
+
next false unless c.receiver.nil? || c.receiver.is_a?(Prism::SelfNode)
|
|
389
|
+
next false unless c.arguments&.arguments&.size == 1
|
|
390
|
+
|
|
391
|
+
arg = c.arguments&.arguments&.first
|
|
392
|
+
next false unless arg.is_a?(Prism::ConstantPathNode)
|
|
393
|
+
next false unless arg.slice.match?(constant_regex)
|
|
394
|
+
|
|
395
|
+
true
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
#: (Array[Prism::Comment]) -> Array[Spoom::RBS::TypeAlias]
|
|
400
|
+
def collect_type_aliases(comments)
|
|
401
|
+
type_aliases = [] #: Array[Spoom::RBS::TypeAlias]
|
|
402
|
+
|
|
403
|
+
return type_aliases if comments.empty?
|
|
404
|
+
|
|
405
|
+
continuation_comments = [] #: Array[Prism::Comment]
|
|
406
|
+
|
|
407
|
+
comments.reverse_each do |comment|
|
|
408
|
+
string = comment.slice
|
|
409
|
+
|
|
410
|
+
if string.start_with?("#:")
|
|
411
|
+
string = string.delete_prefix("#:").strip
|
|
412
|
+
location = comment.location
|
|
413
|
+
|
|
414
|
+
if string.start_with?("type ")
|
|
415
|
+
continuation_comments.reverse_each do |continuation_comment|
|
|
416
|
+
string = "#{string}#{continuation_comment.slice.delete_prefix("#|")}"
|
|
417
|
+
location = location.join(continuation_comment.location)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
type_aliases << Spoom::RBS::TypeAlias.new(string, location)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Clear the continuation comments regardless of whether we found a type alias or not
|
|
424
|
+
continuation_comments.clear
|
|
425
|
+
elsif string.start_with?("#|")
|
|
426
|
+
continuation_comments << comment
|
|
427
|
+
else
|
|
428
|
+
continuation_comments.clear
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
type_aliases
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
#: (Array[Prism::Comment]) -> void
|
|
436
|
+
def apply_type_aliases(comments)
|
|
437
|
+
type_aliases = collect_type_aliases(comments)
|
|
438
|
+
|
|
439
|
+
type_aliases.each do |type_alias|
|
|
440
|
+
indent = " " * type_alias.location.start_column
|
|
441
|
+
insert_pos = adjust_to_line_start(type_alias.location.start_offset)
|
|
442
|
+
|
|
443
|
+
from = insert_pos
|
|
444
|
+
to = adjust_to_line_end(type_alias.location.end_offset)
|
|
445
|
+
|
|
446
|
+
*, decls = ::RBS::Parser.parse_signature(type_alias.string)
|
|
447
|
+
|
|
448
|
+
# We only expect there to be a single type alias declaration
|
|
449
|
+
next unless decls.size == 1 && decls.first.is_a?(::RBS::AST::Declarations::TypeAlias)
|
|
450
|
+
|
|
451
|
+
rbs_type = decls.first
|
|
452
|
+
sorbet_type = @type_translator.translate(rbs_type.type)
|
|
453
|
+
|
|
454
|
+
alias_name = ::RBS::TypeName.new(
|
|
455
|
+
namespace: rbs_type.name.namespace,
|
|
456
|
+
name: rbs_type.name.name.to_s.gsub(/(?:^|_)([a-z\d]*)/i) do |match|
|
|
457
|
+
match = match.delete_prefix("_")
|
|
458
|
+
!match.empty? ? match[0].upcase.concat(match[1..-1]) : +""
|
|
459
|
+
end,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
@rewriter << Source::Delete.new(from, to)
|
|
463
|
+
content = "#{indent}#{alias_name} = T.type_alias { #{sorbet_type.to_rbi} }\n"
|
|
464
|
+
content = pad_out_line_count(of: content, to_height_of: type_alias)
|
|
465
|
+
@rewriter << Source::Insert.new(insert_pos, content)
|
|
466
|
+
rescue ::RBS::ParsingError, ::RBI::Error
|
|
467
|
+
# Ignore type aliases with errors
|
|
468
|
+
next
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# @overridable
|
|
473
|
+
#: (of: String, to_height_of: Spoom::RBS::Comment) -> String
|
|
474
|
+
def pad_out_line_count(of:, to_height_of:)
|
|
475
|
+
replacement = of
|
|
476
|
+
|
|
477
|
+
# no-op implementation
|
|
478
|
+
replacement
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator"
|
|
5
|
+
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options"
|
|
6
|
+
|
|
7
|
+
module Spoom
|
|
8
|
+
module Sorbet
|
|
9
|
+
module Translate
|
|
10
|
+
module RBSCommentsToSorbetSigs
|
|
11
|
+
class HumanReadableTranslator < BaseTranslator
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Deletes the discarded overload from the source codes
|
|
15
|
+
# @override
|
|
16
|
+
#: (Spoom::RBS::Signature) -> void
|
|
17
|
+
def rewrite_discarded_overload(signature)
|
|
18
|
+
from = adjust_to_line_start(signature.location.start_offset)
|
|
19
|
+
to = adjust_to_line_end(signature.location.end_offset)
|
|
20
|
+
@rewriter << Source::Delete.new(from, to)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @override
|
|
24
|
+
#: (
|
|
25
|
+
#| Spoom::RBS::Annotation,
|
|
26
|
+
#| parent_node: PrismTypes::anyScopeNode,
|
|
27
|
+
#| insert_pos: Integer,
|
|
28
|
+
#| sorbet_replacement: String?
|
|
29
|
+
#| ) -> void
|
|
30
|
+
def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:)
|
|
31
|
+
return unless sorbet_replacement # unknown annotation.
|
|
32
|
+
|
|
33
|
+
from = adjust_to_line_start(annotation.location.start_offset)
|
|
34
|
+
to = adjust_to_line_end(annotation.location.end_offset)
|
|
35
|
+
|
|
36
|
+
@rewriter << Source::Delete.new(from, to)
|
|
37
|
+
|
|
38
|
+
indent = " " * (parent_node.location.start_column + 2)
|
|
39
|
+
newline = parent_node.body.nil? ? "" : "\n"
|
|
40
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{sorbet_replacement}#{newline}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @override
|
|
44
|
+
#: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
|
|
45
|
+
def rewrite_type_params_signature(signature, type_params:)
|
|
46
|
+
from = adjust_to_line_start(signature.location.start_offset)
|
|
47
|
+
to = adjust_to_line_end(signature.location.end_offset)
|
|
48
|
+
@rewriter << Source::Delete.new(from, to)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @override
|
|
52
|
+
#: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
|
|
53
|
+
def insert_type_member(type_member, parent_node:, insert_pos:)
|
|
54
|
+
indent = " " * (parent_node.location.start_column + 2)
|
|
55
|
+
newline = parent_node.body.nil? ? "" : "\n"
|
|
56
|
+
@rewriter << Source::Insert.new(insert_pos, "\n#{indent}#{type_member}#{newline}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @override
|
|
60
|
+
#: (String mixin_name, into: Prism::Node, at: Integer) -> void
|
|
61
|
+
def extend_with(mixin_name, into:, at:)
|
|
62
|
+
indent = " " * (into.location.start_column + 2)
|
|
63
|
+
# `extend` is always followed by an annotation or `type_member`, so it always needs a
|
|
64
|
+
# trailing newline to separate them. Since it's never the last inserted line, that
|
|
65
|
+
# trailing newline can't leave a blank line before `end` (unlike the lines that follow).
|
|
66
|
+
@rewriter << Source::Insert.new(at, "\n#{indent}extend #{mixin_name}\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/base_translator"
|
|
5
|
+
require "spoom/sorbet/translate/rbs_comments_to_sorbet_sigs/options"
|
|
6
|
+
|
|
7
|
+
module Spoom
|
|
8
|
+
module Sorbet
|
|
9
|
+
module Translate
|
|
10
|
+
module RBSCommentsToSorbetSigs
|
|
11
|
+
class LineMatchingTranslator < BaseTranslator
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Comments out the discarded overload
|
|
15
|
+
# @override
|
|
16
|
+
#: (Spoom::RBS::Signature) -> void
|
|
17
|
+
def rewrite_discarded_overload(signature)
|
|
18
|
+
@rewriter << Source::Insert.new(signature.location.start_offset + 1, " RBS_DISCARDED_OVERLOAD")
|
|
19
|
+
|
|
20
|
+
signature.continuation_locations.each do |location|
|
|
21
|
+
@rewriter << Source::Insert.new(location.start_offset + 1, " RBS_DISCARDED_OVERLOAD:")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @override
|
|
26
|
+
#: (
|
|
27
|
+
#| Spoom::RBS::Annotation,
|
|
28
|
+
#| parent_node: PrismTypes::anyScopeNode,
|
|
29
|
+
#| insert_pos: Integer,
|
|
30
|
+
#| sorbet_replacement: String?
|
|
31
|
+
#| ) -> void
|
|
32
|
+
def apply_class_annotation(annotation, parent_node:, insert_pos:, sorbet_replacement:)
|
|
33
|
+
case annotation.string
|
|
34
|
+
when /^@requires_ancestor: /
|
|
35
|
+
@rewriter << Source::Replace.new(
|
|
36
|
+
annotation.location.start_offset,
|
|
37
|
+
annotation.location.end_offset,
|
|
38
|
+
"# RBS_REWRITTEN_ANNOTATION: #{annotation.string}\n",
|
|
39
|
+
)
|
|
40
|
+
else
|
|
41
|
+
rewrite_annotation(annotation, is_known: !!sorbet_replacement)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if sorbet_replacement
|
|
45
|
+
@rewriter << Source::Insert.new(insert_pos, "; #{sorbet_replacement}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @override
|
|
50
|
+
#: (Spoom::RBS::Signature, type_params: Array[::RBS::AST::TypeParam]) -> void
|
|
51
|
+
def rewrite_type_params_signature(signature, type_params:)
|
|
52
|
+
# Rewrite `#: [A, B]` into `# RBS_WRITTEN_ANNOTATION: [A, B]`
|
|
53
|
+
@rewriter << Source::Replace.new(
|
|
54
|
+
signature.location.start_offset,
|
|
55
|
+
signature.location.start_offset + 1, # the `#:` prefix
|
|
56
|
+
"# RBS_WRITTEN_ANNOTATION:",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Rewrite each continuation line `#| B]` into `# RBS_WRITTEN_ANNOTATION: B]`
|
|
60
|
+
signature.continuation_locations.each do |location|
|
|
61
|
+
@rewriter << Source::Replace.new(
|
|
62
|
+
location.start_offset,
|
|
63
|
+
location.start_offset + 1, # the `#|` continuation prefix
|
|
64
|
+
"# RBS_WRITTEN_ANNOTATION:",
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @override
|
|
70
|
+
#: (String type_member, parent_node: PrismTypes::anyScopeNode, insert_pos: Integer) -> void
|
|
71
|
+
def insert_type_member(type_member, parent_node:, insert_pos:)
|
|
72
|
+
@rewriter << Source::Insert.new(insert_pos, "; #{type_member}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @override
|
|
76
|
+
#: (Spoom::RBS::Annotation, is_known: bool) -> void
|
|
77
|
+
def rewrite_annotation(annotation, is_known:)
|
|
78
|
+
annotation_start = annotation.location.start_offset + 1 # skip past the `#`
|
|
79
|
+
text = is_known ? " RBS_REWRITTEN_ANNOTATION:" : " RBS_IGNORED_UNKNOWN_ANNOTATION:"
|
|
80
|
+
@rewriter << Source::Insert.new(annotation_start, text)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @override
|
|
84
|
+
#: (String mixin_name, into: Prism::Node, at: Integer) -> void
|
|
85
|
+
def extend_with(mixin_name, into:, at:)
|
|
86
|
+
insert_pos = at
|
|
87
|
+
|
|
88
|
+
@rewriter << Source::Insert.new(insert_pos, "; extend #{mixin_name}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @override
|
|
92
|
+
#: (of: String, to_height_of: Spoom::RBS::Comment) -> String
|
|
93
|
+
def pad_out_line_count(of:, to_height_of:)
|
|
94
|
+
original_line_count = to_height_of.location.end_line - to_height_of.location.start_line + 1
|
|
95
|
+
replacement_line_count = of.count("\n")
|
|
96
|
+
needed_padding_lines = original_line_count - replacement_line_count
|
|
97
|
+
return of if needed_padding_lines == 0
|
|
98
|
+
|
|
99
|
+
if needed_padding_lines < 0
|
|
100
|
+
raise <<~MSG
|
|
101
|
+
Replacement content has more lines than the original content.
|
|
102
|
+
Original:
|
|
103
|
+
#{to_height_of.string}
|
|
104
|
+
Replacement content:
|
|
105
|
+
#{of}
|
|
106
|
+
MSG
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
of + "\n" * needed_padding_lines
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|